(alpacahack)AlpacaHack Round 1 (Pwn) echo(abs(INT_MIN)整数溢出绕过检查,栈溢出)
本文最后更新于 12 天前,其中的信息可能已经有所发展或是发生改变。

网址(内容来自互联网,风险自担)

https://alpacahack.com/ctfs/round-1/challenges/echo

题目

做法

下载压缩包,解压

开虚拟机checksec(不知道是哪个文件的进虚拟机拖文件的时候自然就知道了,是echo,熟悉的文件标志)

64位,没开栈保护

这里,我们看到压缩包解压后还有个C源文件,我们点进去看看(可以下vsCode等工具打开)

完整代码如下

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUF_SIZE 0x100

/* Call this function! */
void win() {
  char *args[] = {"/bin/cat", "/flag.txt", NULL};
  execve(args[0], args, NULL);
  exit(1);
}

int get_size() {
  // Input size
  int size = 0;
  scanf("%d%*c", &size);

  // Validate size
  if ((size = abs(size)) > BUF_SIZE) {
    puts("[-] Invalid size");
    exit(1);
  }

  return size;
}

void get_data(char *buf, unsigned size) {
  unsigned i;
  char c;

  // Input data until newline
  for (i = 0; i < size; i++) {
    if (fread(&c, 1, 1, stdin) != 1) break;
    if (c == '\n') break;
    buf[i] = c;
  }
  buf[i] = '\0';
}

void echo() {
  int size;
  char buf[BUF_SIZE];

  // Input size
  printf("Size: ");
  size = get_size();

  // Input data
  printf("Data: ");
  get_data(buf, size);

  // Show data
  printf("Received: %s\n", buf);
}

int main() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  echo();
  return 0;
}

还是先找main函数,在最后,没啥东西,但我们看到它调用了一个echo函数,找到这个函数看看

int main() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  echo();
  return 0;
}

定义了一个size,声明了一个字符数组 buf,数组大小由BUF_SIZE 决定,我们先找这个size看看

void echo() {
  int size;
  char buf[BUF_SIZE];

打印”Size:“,然后让size = get_size(),即调用 get_size() 这个函数,把它返回的值赋给变量 size

那我们先找到get_size看看

  // Input size
  printf("Size: ");
  size = get_size();

定义size = 0,然后读取一个十进制整数,并将其存储到 size 变量中

%d 是格式说明符(用于读取或输出十进制整数))

%*c

  • %c 表示读取一个字符
  • * 表示“读了但丢弃,不赋值

比如输入: 123↵

执行后:

  • size = 123
  • 回车那个 '\n'%*c 读走并丢弃

常见用途: 避免下一次输入时,缓冲区里还残留一个换行符。

int get_size() {
  // Input size
  int size = 0;
  scanf("%d%*c", &size);

看到这没提到啥函数了,顺势往下看

在第20行我们看到abs,怀疑是否为Abs有符号整数溢出

让用户输入的数变成绝对值,查看是否大于第5行定义的BUF_SIZE(0x100)

转换一下进制,0x100=256(十进制)

若大于,则异常退出(exit(0)进程正常终止,exit(1)进程异常终止)

  // Validate size
  if ((size = abs(size)) > BUF_SIZE) {
    puts("[-] Invalid size");
    exit(1);
  }

Abs有符号整数溢出

对于 32 位 int:

INT_MIN = -2147483648 INT_MAX = 2147483647

数学上:

abs(INT_MIN) = abs(-2147483648) = 2147483648

但是 2147483648 超过了 int 能表示的最大值 2147483647,因此无法用 int 表示。

所以 abs(INT_MIN) 会产生整数溢出问题。

从 C 标准角度看,这是未定义行为;但在常见 CTF 环境中,abs(INT_MIN) 的结果通常仍然是 INT_MIN,也就是 -2147483648。

因此代码:

if ((size = abs(size)) > BUF_SIZE)

在输入 INT_MIN 时,可能变成:

if (-2147483648 > BUF_SIZE)

由于 BUF_SIZE 是正数,所以这个判断为假,程序不会进入 Invalid size 分支,从而绕过 size 检查。

具体流程就是:

输入 INT_MIN
        ↓
abs(INT_MIN) 溢出
        ↓
常见环境下 size 仍然是 INT_MIN,也就是负数
        ↓
size > BUF_SIZE 判断失败
        ↓
绕过检查

然后,我们再看看其他没看到的函数

get_data(buf, size) 会把我们输入的数据逐字节写入 buf,而 size 用来控制最多写入多少字节。 由于 buf 在栈上,且程序没开 Canary,因此我们可以考虑一下栈溢出

  // Input data
  printf("Data: ");
  get_data(buf, size);

然后我们来看看这个for循环

fread 的格式是: fread(读到哪里, 每次读多大, 读多少个, 从哪里读)

因此,这里就是:

  • 把数据读到 c 这个变量里
  • 每次读 1 字节
  • 1
  • stdin 读,也就是标准输入(键盘/管道输入)

所以这句其实就是: 每次读一个字符到 c 里。

然后这一行代表的就是如果没成功读到 1 个字节,就退出循环。 相当于: 读不到东西就别读了。

然后就来到 if (c == '\n') break; 如果读到的是换行符,也退出循环。

再来,就是buf[i] = c; 如果成功读到了字符,而且不是换行,就把这个字符放进缓冲区:

最后,我们一起来看这里 buf[i] = '\0'; 这句非常重要。 它是在最后面补一个: ‘\0’ 这叫 字符串结束符。 因为 C 语言的字符串必须以 \0 结尾,像这样: ‘a’ ‘b’ ‘c’ ‘\0’ 这样后面如果 printf("%s", buf);,程序才知道字符串到这里结束。

  // Input data until newline
  for (i = 0; i < size; i++) {
    if (fread(&c, 1, 1, stdin) != 1) break;
    if (c == '\n') break;
    buf[i] = c;
  }
  buf[i] = '\0';
}

然后看到第8行的win函数,我们看到”/bin/cat”和”/flag.txt”,也许可以当做我们的后门函数,我们接着看

如果 execve 调用成功,当前进程会开始执行 /bin/cat 程序,并显示 /flag.txt 文件的内容,因此,我们exp最后与靶机交互的命令也要改改

然后下面来详细解释一下,这里也涉及到一些指针的问题了

  1. args

是整个数组名

里面装了n个指针,无论这些指针是什么层级的,有多少,相当于一个集合打包成一个名字

  1. args[0]

是数组第 0 个元素

它是一个 char *,char类型的一级指针,在这里也就是:

指向字符串 "/bin/cat" 的地址

C 字符串本体是一串连续的 char,并且以 ‘\0’ 结尾 我们通常用这串字符的首地址来表示、传递、访问这个字符串

也就是说:

"/bin/cat"

在内存里大概是:

'/' 'b' 'i' 'n' '/' 'c' 'a' 't' '\0'

args[0] 存的是这串字符第一个字符 '/' 的地址

C 字符串 的规则是:

  1. 从一个地址开始
  2. 这个地址后面是一串 char

如果是用 printf 打印 args[0] 指向的整串字符串,就要用 %s

  • %s 的规则是:从这个地址开始,一个字符一个字符往后读
  • 一直读到 '\0' 为止 所以它才把整串打印出来

PS:通常,我们的指针都是满足这样一个规律:

一个指针类型:

int **********p; // 管它多少个 *
  • p地址
  • *p地址
  • **p地址
  • ……
  • 写到 星号数量跟定义一样多才是值

不管你叠多少层 *永远只有最后那一次解引用才是值,前面全都是地址。

也就是:

每解引用一次,类型就少一层 *。

如果少完之后类型里还有 *,那它还是指针,也就是地址。 如果少完之后类型里没有 *,那它就是普通数据。

这样,我们也很好解释第三点了

  1. *args[0]

类型是char,是把 args[0] 这个地址解引用,得到那个地址上的第一个字符

然后就是

execve(args[0], args, NULL);

这是系统调用,作用是: 让当前程序变成另一个程序来执行

函数原型大概是: execve(程序路径, 参数数组, 环境变量)

所以这里就是: execve(args[0], args, NULL);

展开后相当于: execve(“/bin/cat”, {“/bin/cat”, “/flag.txt”, NULL}, NULL);

也就是:

  • 要执行的程序:/bin/cat
  • 传给它的参数:/bin/cat/flag.txt
  • 环境变量:NULL

效果就等于在命令行运行: /bin/cat /flag.txt

exit(1); 这个意思是: 如果前面的 execve 失败了,就退出程序

因为 execve 有个特点:

  • 成功了,就不会返回
  • 失败了,才会继续往下执行

即:

  • 如果 execve 成功:当前进程直接被替换成 /bin/cat,后面的 exit(1) 根本执行不到
  • 如果 execve 失败:execve 返回,程序才会继续执行 exit(1)

因此,这里就是,先搞一个一级指针数组,并写好东西进去 然后利用execve去执行 成功了,打印flag 失败了,异常退出

  1. 为什么 args[0]"/bin/cat"

首先,不能写成*args[0]

  • args[0] 的类型是 char *
    • 含义:指向字符串 "/bin/cat" 开头的地址
  • *args[0] 的类型是 char
    • 含义:这个字符串的第一个字符,也就是 '/'

假设: char *p = “/bin/cat”;

那么:

  • p"/bin/cat" 的地址
  • *p'/'
  • p[0]'/'
  • p[1]'b'

所以: printf(“%s\n”, p); // /bin/cat
printf(“%c\n”, *p); // /

因此,这里不能写成 *args[0]。

args[0] 的类型是 char *,它存的是字符串 “/bin/cat” 的首地址,也就是字符 ‘/’ 的地址。

而 *args[0] 的类型是 char,它表示对 args[0] 这个地址解引用,取出字符串的第一个字符,也就是 ‘/’。

在 C 语言里,字符串通常用首地址来表示。只要某个函数按照 C 字符串规则使用这个首地址,它就会从这个地址开始一直读取字符,直到遇到 ‘\0’ 为止。

所以严格来说,args[0] 不是字符串本体,而是指向字符串 “/bin/cat” 的首地址。

但为了方便理解,我们经常会口语化地说:

args[0] 是 “/bin/cat”

更严谨地说应该是:

args[0] 指向字符串 “/bin/cat”。

  • argv参数数组的首地址
  • argv[0]第一个字符串的首地址
  • *argv[0]第一个字符串的第一个字符

若想表示/bin/cat的b的话,可以写成: *(p + 1)

或者更常见地写成: p[1]

这两个是一样的

p[0] 是对 整个变量 p 做下标。

它等价于: *(p + 0)

也就是: *p

所以:

  • p[0] 不是“*p 里面的 p”
  • p[0] 是“从 p 指向的位置开始,取第 0 个元素”

一级指针:变量里存一个地址,这个地址指向普通数据。

二级指针:变量里也只存一个地址,只不过这个地址指向的是“一级指针”。 二级指针可以指向一个一级指针变量,也可以指向一片连续的一级指针数组。

三级指针:变量里也只存一个地址,只不过这个地址指向的是“二级指针”。 三级指针可以指向一个二级指针变量,也可以指向一片连续的二级指针数组。 如果它指向一片连续的二级指针数组,那么它存的就是这个二级指针数组第一个元素的地址。
通过 r[0]、r[1]、r[2] 可以取出其中某一个二级指针。
取出来的这个二级指针,又可以继续指向一个一级指针变量,也可以指向一片连续的一级指针数组。

四级指针:变量里也只存一个地址,只不过这个地址指向的是“三级指针”。 更高层指针也是同样规律。

char *p = “/bin/cat”;

这里 p 是一级指针。

它里面只存一个地址,比如假设是 1000

这个地址 1000 指向哪里?

指向字符串开头:

地址1000: ‘/’
地址1001: ‘b’
地址1002: ‘i’
地址1003: ‘n’

所以:

  • p 里确实只有 一个地址
  • 但这个地址后面可以连着很多字符
  • 因此它可以表示“整串字符串的开头”

所以“一级指针只能有一个”更准确地说是:

一级指针变量里只能存一个地址值。
但这个地址可以指向一大片连续内存。

  1. 首地址和完整地址区别?

严格说: “首地址”和“完整地址”这种说法里,真正标准、常用的只有“首地址”。
“完整地址”不是 C 里很标准的术语。

你这里更准确应该理解成:

  • 首地址:这块数据从哪里开始
  • 整块数据:占了一段连续内存,不是只有一个地址

e.g.假设字符串 "/bin/cat" 在内存里是这样:

地址1000: ‘/’
地址1001: ‘b’
地址1002: ‘i’
地址1003: ‘n’
地址1004: ‘/’
地址1005: ‘c’
地址1006: ‘a’
地址1007: ‘t’
地址1008: ‘\0’

那:

  • 这个字符串的首地址1000
  • 它占的内存范围是 1000 ~ 1008

所以你要是说“这个字符串的地址”,一般默认就是说:

它的首地址 1000

不是说它只有一个字节,而是说: 整块字符串就用起始地址来代表。

  1. 两个NULL分别是啥?

第一个NULL是结束标记,不然程序不知道参数表到哪停 第二个NULL是环境变量,意思是:不给新程序传环境变量

  1. 为什么/bin/cat要作为参数传给/bin/cat?要同时出现两次?

前一个 /bin/cat 是“执行目标”,后一个 /bin/cat 是“传给程序自己的名字” argv[1] = "/flag.txt":传给程序的文件名(后面也都是这个了,只有第一个参数是传给程序自己的名字)

因为程序启动以后,它自己并不知道你是怎么把它叫起来的

所以系统会顺手把一份“参数表”交给它,其中第一个位置通常放:

程序被调用时使用的名字,也就是 argv[0]

也就是“这次你是以什么名字被调用的”。

有些程序会根据“自己叫什么”改变行为

有些程序非常典型,比如一个程序文件可能有多个名字:

  • 叫这个名字时干这个事
  • 叫另一个名字时干另一个事

也就是说,程序名本身也可能是输入的一部分

void win() {
  char *args[] = {"/bin/cat", "/flag.txt", NULL};
  execve(args[0], args, NULL);
  exit(1);
}

其他就没啥了(这个C源文件跟IDA反编译后内容差不多,主要是找C源文件找到疑点却没有的东西,比如buf,考虑栈溢出)

扔进IDA(64位),找到main,F5反编译

上面分析过,没啥东西,其他函数点进去也没啥东西,我们点进echo函数看看

我们看到了上面分析的get_size和get_data函数

上面分析到 get_data 会根据 size 控制读取长度,并把输入内容写入 buf,因此我们需要计算从 buf 到返回地址的偏移。然后我们去看buf的栈,就是这里的&v2,算出来是280

注: 一般算这种还是建议直接在计算器上算吧(打开电脑自带计算器->点击左上角三条杠->选择程序员->选择HEX->输入buf+saved rbp的大小->看DEC也就是十进制的数字,然后把这个数字写进exp即可)

我之前的一些笔记上面是直接用十六进制的十位数*16加上十六进制的个位数转换成十进制数的大小,然后再加上saved rbp的值,算出来的结果,我也有演示,这种算法在特定情况下这个是可以算对的,但是三位以上的十六进制算法就不能这样算了,还是采取我上面说的直接用计算器算出

防止误解,给点例子:

两位十六进制可以直接按:

0x28 = 2 * 16 + 8 = 40

但是三位以上时,不能把前面的几位当成十进制。

例如:

0x118 不能算成 11 * 16 + 8。

正确是:

0x118 = 0x11 * 16 + 0x8

其中 0x11 是十六进制的 11,等于十进制 17。

所以:

0x118 = 17 * 16 + 8 = 280

也可以展开成:

0x118 = 1 * 16^2 + 1 * 16 + 8 = 256 + 16 + 8 = 280

这里的^不是异或是多少次方

recvall() 的意思是:

一直接收目标程序输出的数据,直到对方关闭连接/进程结束。

写exp之前nc一下看看什么时候发送东西造成溢出

exp

#导入pwn模块
from pwn import *

#与靶机进行连接
r = remote("34.170.146.252",11081)

#合适位置进行abs有符号整数溢出
r.sendlineafter(b"Size:",b"-2147483648")

#合适位置发送字节数据和后门函数win
r.sendlineafter(b"Data:",b'A'*280 + p64(0x4011F6))

#打印返回的flag
print(r.recvall())

常规,得出flag——Alpaca{s1Gn3d_4Nd_uNs1gn3d_s1zEs_c4n_cAu5e_s3ri0us_buGz}

感谢阅读!如果你觉得这篇文章对你有帮助,欢迎扫码赞赏支持,你的鼓励是我持续创作的动力 ❤️

本文为原创内容,转载请注明出处。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇