编辑
2025-01-22
工作知识
0

我们在讨论栈破坏的时候,通常是数组越界访问和库函数memset/memcpy等函数的错误覆盖,对于这样的错误,通常大家知道原因排查起来很方便,但是实际情况中,还有第三种栈破坏的问题,就是结构体不匹配的类型转换导致的栈破坏。本文讨论这种栈破坏问题。

一、发生情况

通常情况下,结构体的类型转换如果出现错误,编译的时候就会提示,例如将两个不同的结构体进行类型转换,如:

struct A a = (struct B)b;

这种错误编译器就告诉你不能这样转换。

但是如果是结构体指针,那么或许可以转换,例如

struct B *b; struct A* a = (struct A*)b;

我们发现可以将struct B的b指针强制类型转换成struct A,这种情况下编译器会提示warning

如果这种强制类型转换出现破坏了,我们查一下编译时的警告就一目了然了。

但是我们还有一种渠道,就是先将结构体指针struct B*转换成void *,然后将void*的变量转换成struct A*,如下:

struct B *b; void* t = b; struct A* a = (struct A*)t;

这种情况下,我们编译代码,将得不到任何警告或错误,因为对于C而言,这种转换本身就是有意义的。

但是如果我们代码直接这样写,那么当我们出现栈破坏的时候,我们可以通过审查代码的方式定位和解决问题。

如果我们的代码做过封装,封装的时候考虑了解耦,这种情况下,我们可以发现

struct B* 出现在服务层,我们需要在具体实现时构造struct B void* t=b 出现在接口层,为了解耦,我们需要将类型转换成通用的void struct A* 出现在客户端,我们在实现应用的时候,需要实例化一个对象,这个对象是struct A*的结构

就像这样

image.png 这种情况下,我们不方便追查问题,因为其真实的调用情况如下:

image.png 我们的app程序在调用libxxx.so时,根据的是libxxx_api.h,并不知道libxxx.so内部的实现,而app程序又会自行构造和类型转换结构体。

也就是说,如果libxxx.so的结构体和app的结构体声明存在不一致,那么可能出现问题。

那么有什么情况呢?这里以struct A和struct B为例,struct A在libxxx.so中实现,struct B在application中实现

image.png

  • 假设A的成员比B多,那么最后强制类型转换为B,我们可以发现是将实际结构体成员缩小了。并不会出现问题
  • 假设A的成员比B少,那么最后强制类型转换为B,我们可以发现B在访问变量的时候,就会错误的访问到栈区或堆区(取决于申请情况)
  • 假设A的结构体和B的结构体完全不一样,那么应用在访问数据的时候就能感知到业务出现了差异,问题很容易发现,所以不做讨论
  • 假设A的结构体和B的结构体完全一样,那么这属于正常行为,不会出现任何问题

根据上面所述的,我们存在问题的情况只有在2的时候,可能发生。这里为什么说可能发生,而不是一定发生呢,因为需要满足以下两点

  • A应该在栈区,栈区的数据才能破坏,如果在堆区,那么破坏的是堆区的数据
  • A应该破坏了自己或其他栈区结构体指针

如果A破坏了堆区,那么会出现堆区的异常,这里不做讨论

如果A只是占用了没分配的栈地址,那么程序不会异常

如果A只是修改了某个栈的局部变量,函数参数和临时数据,那么程序可能不会出现异常

所以这里的破坏,一定是破坏了某个栈区的结构体指针。

二、这种情况的发生场景

根据上面提到的,我们想要出现这种错误,那么我们肯定做了void*的转换,而实际调用so的过程中,我们其实没必要非做void*的类型转换,直接包含同一个h即可。那么什么场景下,这种转换尤为重要呢?

答案是这个数据来源于内核,因为内核的结构体和应用的结构体没有做好绑定。内核和应用又是隔离的。所以void*的转换尤为重要。

所以,综上可以知道。 如果我们通过内核拿一个结构体A,应用层的结构体B需要做强制类型转换,如果A和B结构体是完全一致的,那么不会存在问题,假设如下两种情况:

  1. 内核未更新,应用发生了更新,将结构体B的成员添加了,从而导致结构体B的成员比A多。
  2. 内核发生了更新,将结构体A的成员减少了,应用未发生更新,从而导致结构体B的成员比A多。

至此,我们将这种栈破坏讲述的比较清楚了。也可以发现,这种栈破坏在满足一定条件下,实际上是很容易出现的。

三、示例

3.1 直接复现问题

为了阐述这个问题,我们先编写了一个简单的c程序,该程序直接进行强制类型转换如下:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> struct kernel{ int x; int y; int z; }; struct user{ int x; int y; int z; int o1; int o2; int o3; int o4; }; void test1(struct user* bug) { bug->x = 2; return; } void test(struct kernel k) { struct user* s = (struct user*)&k; s->o4 = 1; test1(s); return ; } int main(int argc, char *argv[]) { struct user arr = {1,2,3}; void* ss = &arr; struct kernel *b = (struct kernel*)ss; test(*b); return 0; }

我们将其编译:

gcc /root/stack_damage.c -o stack_damage

此时运行后。我们拿到一个段错误如下:

# ~/stack_damage 段错误 (核心已转储)

这里值得注意的是如下:

struct user arr = {1,2,3}; 声明了一个user的结构体 void* ss = &arr; 将其转换成void* struct kernel *b = (struct kernel*)ss; 将其强制类型转换成kernel void test(struct kernel k) 函数test会主动构造kernel结构体 struct user* s = (struct user*)&k; 此时强制类型转换给了s s->o4 = 1; 通过s破坏了结构体s自己 bug->x = 2; 这里访问自己的x时,出现了栈破坏的错误

这里还需要两个知识点如下:

  • 函数形参如果不超过16字节,那么不会使用x19来计算偏移,而是直接通过sp的偏移存储,也就是x0x1x2x3存放sp的偏移存储,也就是x0,x1,x2,x3存放sp开头的位置

9b13ee5fc9e85cc28c462df5ec1355c.jpg 上述查看B.4

  • 函数的第一个局部变量使用的是sp+sp+offset - 4,也就是第一个局部变量在栈区的最高位

如果忽略这两个要素,我们不会出现必现的栈破坏问题。

我们通过gdb来调试如下:

# gdb ./starck_damage GNU gdb (Ubuntu 9.1-0kylin1) 9.1 Copyright (C) 2020 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "aarch64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./starck_damage... (gdb)

此时运行如下:

(gdb) r Starting program: /root/stack_panic/starck_damage [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Program received signal SIGSEGV, Segmentation fault. 0x0000000000400554 in test1 (bug=0x7f00000001) at /root/starck_damage.c:26 26 bug->x = 2;

然后我们得到了一个巨大的疑问

为什么bug->x =2 会出现段错误 此时我们应该现打一个断点test,然后重跑到断点如下:

(gdb) b test Breakpoint 1 at 0x40057c: test. (2 locations) (gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/stack_panic/starck_damage [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Breakpoint 1, test (k=...) at /root/starck_damage.c:32 32 struct user* s = (struct user*)&k;

然后定位到s->o4 = 1;之前,

(gdb) b *0x0000000000400588 Breakpoint 1 at 0x400588: file /root/starck_damage.c, line 33.

然后打印s的地址如下:

(gdb) p &s $1 = (struct user **) 0x7fffffeff8

然后计算s保存的值

(gdb) p s $2 = (struct user *) 0x7fffffefe0

我们得到地址0x7fffffefe0,然后查看其值

(gdb) x/3w 0x7fffffefe0 0x7fffffefe0: 0x00000001 0x00000002 0x00000003

此时我们将其定位到s->o4 = 1;运行之后,

(gdb) b *0x0000000000400590 Breakpoint 2 at 0x400590: file /root/starck_damage.c, line 34.

此时我们打印s的值,如下

(gdb) p s $4 = (struct user *) 0x7f00000001

可以发现s的值变成了0x7f00000001。此时我们访问

bug->x = 2;

自然出现段错误。

那么为什么会这样呢?

我们可以看到k的地址如下:

(gdb) p &k $7 = (struct kernel *) 0x7fffffefe0

然后s的地址是0x7fffffeff8

我们拿0x7fffffeff8-0x7fffffefe0=24

24/4+1=7

也就是说我们如果操作user的第7个int,那么就会破坏s的栈。

所以这也就是s->o4 = 1;的由来。

至此,我们通过直接的示例阐述了破坏栈区的一种情况

3.2 真实的情况

根据上面说的,我们为了复现问题编写了c程序,但是真实的情况应该来自内核的结构体和应用的结构体不一致,所以我们先编写内核驱动如下:

struct data { int x; int y; int z; }; static long kylin_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct data user_data; user_data.x = 1; user_data.y = 2; user_data.z = 3; switch(cmd) { case RD_VALUE: if (copy_to_user((void __user *)arg, &user_data, sizeof(user_data))) { pr_err("Data Read : Err!\n"); } break; default: pr_info("Default\n"); break; } return 0; }

此时我们加载ko会在/dev/下出现kylin的字符设备

# ls /dev/kylin /dev/kylin

我们应用上先提供ioctl.c如下

#include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include "libs.h" #define RD_VALUE _IOR('a','b',int32_t*) void load_data(void* d) { struct kdata *b = (struct kdata*)d; test(*b); } int main() { int fd; struct kdata d; fd = open("/dev/kylin", O_RDWR); if(fd < 0) { return 0; } ioctl(fd, RD_VALUE, (struct kdata*) &d); printf("%d %d %d\n", d.x, d.y, d.z); load_data(&d); close(fd); }

值得注意的是这里的头文件如下:

root@kylin:~/stack_panic# cat libs.h kernel.h #include <stdio.h> #include <stdlib.h> #include <string.h> #include "kernel.h" void test(struct kdata k); #ifndef _KERNEL_STRUCT_H #define _KERNEL_STRUCT_H struct kdata { int x; int y; int z; }; #endif

我们知道应用需要调用test,所以实现libs.c如下

#include "libs.h" struct data { int x; int y; int z; int o1; int o2; int o3; int o4; }; void oops(struct data* bug) { bug->x = 2; return; } void test(struct kdata k) { struct data* s = (struct data*)&k; s->o4 = 1; oops(s); return ; }

编译如下:

gcc ioctl.c libs.c -g -o ioctl

此时运行时保存如下:

# ./ioctl 1 2 3 段错误 (核心已转储)

可以发现栈出现了破坏。

这里为了简化,我只是简单抽象了test的实现在libs.c中。但其意思符合上文描述的情况。

也就是说,应用如果调用了库,库的结构体和内核结构体不符合,那么就会出现栈破坏。

四、总结

本文通过示例的方式演示了一种特殊的栈破坏情况,这种情况在内核和应用之间非常容易出现,也不容易排查。后续如果有其他情况出现这样的问题的时候,我们可以拿这种情况对号入座一下,这样就不用浪费更多的时间来排查栈破坏的问题了。

编辑
2025-01-22
工作知识
0

根据yocto版本构建(二)安装已有二进制和yocto版本构建(三)源地址dsc进行构建我们可以通过安装源的deb文件和通过dsc来执行一次构建操作,我们知道,通常情况下,代码都是通过git管理,这里还需要提供一个函数支持git方式的构建,如下:

一、bbfile

对于gitlab2拉取源码的方式,我们需要指向到自己的SRC_URI后,根据deb.bbclass来进行代码构建,所以bbfile如下:

inherit kylin inherit deb SRC_URI = "git@gitlab2.kylin.com:shanghai-team/embedded/kylin-egf/ukui-menu.git;branch=egf/v101-tablet" S = "${WORKDIR}/ukui-menu" do_compile() { dpkg-buildpackage -uc -us : }

这里需要注意的是,我们没有使用yocto的本身git构建和打包deb的方式,我们使用了自己的命令,然后,我们需要在do_install的时候,进行dpkg -i的操作,这样sysroot才能正常安装。

二、一些变量解释

为了使得调试更方便,这里提供了一些变量的说明,可以链接查看如下:

https://pages.openeuler.openatom.cn/embedded/docs/build/html/master/yocto/yocto_quick_start_manual/variables_and_tasks.html

主要解释如下:

MACHINE: 指定使用的硬件配置文件,通常在local.conf文件定义; DISTRO: 指定使用的发行版配置文件,通常在local.conf文件定义; PN: 软件包名,一般是根据文件名自动生成;除了一些交叉编译的包,如gcc-cross会在bb中重新定义; PV: 软件包版本; PR: 食谱的修订,默认为r0;当包管理器在已构建的镜像上动态安装包时,PR很重要,当前openEuler未启用; BPN: 软件包名,去除指定的前后缀(如-native、-cross等); BP: ${BPN}-${PV}; SRC_URI: 源码路径,可以为上游或者本地文件路径,上游源码需要使用校验值; LICENSE: 配方的源许可证列表,必须设置;如果设置为”CLOSED”则关闭; LIC_FILES_CHKSUM: 配方源代码中许可证文本的校验和,与LICENSE变量配合使用; PACKAGE_ARCH: 生成包的体系结构; TARGET_VENDOR: 指定目标供应商的名称,openEuler设置为”-openeuler”; TARGET_OS: 指定目标的操作系统; MULTIMACH_TARGET_SYS: 生成包的目标系统类型的唯一标识,默认为${PACKAGE_ARCH}${TARGET_VENDOR}-${TARGET_OS}; WORKDIR: 构建配方的工作目录的路径名,指向${TMPDIR}/work/${MULTIMACH_TARGET_SYS}/${PN}/${EXTENDPE}${PV}-${PR},EXTENDPE变量通常不被设置; S: 构建过程中源代码位置,默认为${WORKDIR}/${BPN}-${PV}; B: 构建过程中生成对象所在的目录,默认与S相同;一些类会将B设置为${WORKDIR}/build; D: 相当于 make install 后的目标目录,指向${WORKDIR}/image; PACKAGES: 表示配方创建的包列表; FILES_xxx: 放置在包中的文件和目录列表; PKGD: 要打包的文件的目录,指向${WORKDIR}/package; PKGDEST: 将文件拆分为单独的包后,指向要打包的文件的父目录,该目录是PACKAGES中指定的每个包的目录,指向${WORKDIR}/packages-split; DEPENDS: 列出配方的构建时依赖关系,配方在构建时需要其它配方的内容(例如头文件和共享库); RDEPENDS: 列出程序包的运行时依赖项,这些依赖项是必须安装的其他程序包,以便程序包正常运行; RECIPE_SYSROOT: 指向${WORKDIR}/recipe-sysroot; RECIPE_SYSROOT_NATIVE: 指向${WORKDIR}/recipe-sysroot-native; SYSROOT_DESTDIR: 指向${WORKDIR}/sysroot-destdir; SYSROOT_DIRS: 暂存到${SYSROOT_DESTDIR}的目录; STAGING_DIR_HOST: 组件运行所在的系统上的sysroot路径,默认为${RECIPE_SYSROOT}。 STAGING_DIR_NATIVE: 构建主机上运行的组件使用的sysroot的路径,默认为${RECIPE_SYSROOT_NATIVE}; STAGING_DIR_TARGET: 当构建在系统上执行的组件并为另一台机器生成代码(例如cross-canadian配方)时使用的sysroot路径; STAGING_KERNEL_DIR: 包含构建树外模块所需的内核头文件的目录(内核源码目录); STAGING_KERNEL_BUILDDIR: 指向包含内核构建工件的目录。需要访问内核构建工件的配方构建软件可以在内核构建后在STAGING_KERNEL_BUILDDIR变量指定的目录中查找这些工件; PACKAGE_CLASSES: 指定构建系统在打包数据时使用的包管理器(例如RPM、DEB或IPK),在local.conf文件设置; IMAGE_ROOTFS: 指定根文件系统在构建过程中的位置( do_rootfs 任务期间)。此变量不可配置,不要更改它; IMAGE_FEATURES: 指定要包含在镜像中的主要功能列表,这些功能大多数都映射到其他安装包; EXTRA_IMAGE_FEATURES: IMAGE_FEATURES的一部分; IMAGE_INSTALL: 指定要安装到镜像中的程序包; PACKAGE_EXCLUDE: :指定不应安装到image中的包; PACKAGE_INSTALL: 要安装到镜像中的程序包的列表,不要更改它,通常使用IMAGE_INSTALL变量间接进行修改; DEPLOY_DIR: 指向构建系统用于放置镜像、包、SDK和其他输出文件的常规区域,这些文件已准备好在构建系统之外使用。默认情况下,此目录位于指向${TMPDIR}/deploy。 DEPLOY_DIR_IMAGE: 指向构建系统用来放置准备部署到目标计算机上的镜像和其他相关输出文件的区域。该目录是特定于机器的默认情况下,此目录指向${DEPLOY_DIR}/images/${MACHINE}/; DEPLOYDIR: 当继承deploy类时,DEPLOYDIR指向已部署文件的临时工作区,默认指向${WORKDIR}/deploy-${PN},此目录内容会被拷贝到${DEPLOY_DIR_IMAGE}; CC: 用于运行C编译器的最小命令和参数; CFLAGS: 指定要传递给C编译器的标志; CXXFLAGS: 指定要传递给C++编译器的标志; CPPFLAGS: 指定要传递给C预处理器(即同时传递给C编译器和C++编译器)的标志; LDFLAGS: 指定要传递给链接器的标志; OVERRIDES: 以冒号分隔的当前应用的覆盖列表。覆盖是一种BitBake机制,允许在解析结束时选择性地覆盖变量; COMPATIBLE_MACHINE: 一种正则表达式,解析为一个或多个与配方兼容的目标机器。可以使用该变量来停止为配方不兼容的机器构建配方,停止这些构建对于内核特别有用。该变量还有助于提高解析速度,因为构建系统会跳过与当前机器不兼容的解析配方。

三、总结

因为我们很多仓库都是通过git管理的,所以通过git地址来触发二进制编译的行为至关重要,根据上述描述,我们可以具备通过git链接地址进行二进制包的构建

编辑
2025-01-22
工作知识
0

根据yocto版本构建(三)源地址dsc进行构建我们可以通过输入一个内部源地址就可以拉取软件包安装,但是我们还需要提供一个功能,如果我们将文件放在recipes内的files下,我们需要手动将其解压和构建,这样可以不依赖源的dsc和文件

一、bbfile

根据上面的描述,我们需要从files中获取文件,如下:

inherit kylin inherit deb SRC_URI = "file://ukui-menu_3.0.1-0720.1.dsc;md5=1b897ae127a2d8076a0b318daa720f91\ file://ukui-menu_3.0.1-0720.1.tar.xz;md5=e684d90a713f953179b1a06e8e3401c1 do_deb_prepend() { install -d ${S} install -m 0755 ${WORKDIR}/ukui-menu_3.0.1-0720.1.dsc${S} install -m 0644 ${WORKDIR}/ukui-menu_3.0.1-0720.1.tar.xz ${S} }

这样可以将ukui-menu的文件放到work目录下,然后我们执行dpkg命令即可正常构建

二、介绍

do_fetch 会使用SRC_URI变量定位源码文件;基于SRC_URI变量值中每个条目的前缀来确定使用哪个提取器来获取源文件, file:// 开头为本地文件, http://git:// 等为上游获取的源文件;

我们这里使用了file,这样将其拉到了sysroot内部,这样我们可以通过自己的函数来执行dpkg-buildpackage。从而构建deb包

其形式类似于yocto版本构建(三)源地址dsc进行构建

三、总结

至此,我们不仅可以通过远程源地址的dsc文件编译二进制包,也可以通过本地file链接来编译二进制包

编辑
2025-01-22
工作知识
0

根据yocto版本构建(二)安装已有二进制已经可以具备安装已有的二进制,这样一个完全仿照livebuild的方式(kybuilder)的yocto工程只需要花点时间就可以配置出来,但是我们不止如此,我们知道,麒麟v10操作系统软件包的开发是和deb开发方式一致的,通过dsc即可正常构建,如下是实现路径

一、deb.bbclass

为了支持debian包的构建,我们需要根据deb的安装方式设计一个bbclass用作bbfile的调用,我们知道,针对系统的软件开发,主要步骤如下:

wget https://xxxx.dsc dpkg -x xxxx.dsc dpkg-buildpackage -uc -us / dh_make

为了让yocto上也能使用dsc来通过源码的方式构建出一个deb包,我们能需要定义函数类似如下:

do_unpack_dsc() { dpkg-source -x ${DL_DIR}/${PN}_${PV}.dsc ${S} } do_compile_dsc() { dpkg-buildpackage -uc -us }

不仅如此,我们还需要在do_package中将其生成的deb文件,复制到recipes的work目录下。这样我们不需要使用yocto的本身构建方式来生成deb

二、bbfile

对于bbfile,我们需要继承我们的类,然后提供一个宏定义,由我们的deb.bbclass获取代码开始构建,如下:

inherit kylin inherit deb DSC_URI = "https://dev.kylinos.cn/kylin-desktop/+archive/primary/+files/ukui-menu_3.0.1-0720.1.dsc;md5sum=1b897ae127a2d8076a0b318daa720f91"

三、调试

为了让我们的bbfile支持调试,我们可以通过如下命令

bitback ukui-menu -c listtasks

这样可以列出我们的步骤,方便调试细节

如果我们需要进入devshell中,具体调试dpkg-buildpackage -uc -us出现的错误,我们可以如下

bitback ukui-menu -c devshell

这样我们可以手动运行chroot进入此环境

四、结论

至此,我们可以通过指定一个dsc文件,就可以触发yocto执行源码的编译动作

编辑
2025-01-22
工作知识
0

我们知道yocto是基于源码构建的工具,如果我们在开发系统的时候,直接使用全部构建的方式来生成操作系统,那代价将会异常的大。所以可以通过二进制安装

一、提供sysroot环境

默认情况下,yocto会根据编译来构建一个sysroot,而每个程序都有自己单独的sysroot作为隔离。这种情况下,如果我们不需要通过构建的方式产生sysroot,我们需要如下:

在meta-kylin下定义一个recipes-debootstrap 运行debootstrap命令,通过麒麟发布的源地址,构建一个chroot环境,此环境是SYSROOT_DESTDIR环境变量 yocto默认使用此sysroot作为版本构建 对于的bb如下:

do_build() { sudo -E debootstrap --variant=minibase --include=systemd,apt kylin ${SYSROOT_DESTDIR} ${KYLIN_REPO} sudo -E chroot ${SYSROOT_DESTDIR} apt-get update sudo -E chroot ${SYSROOT_DESTDIR} apt-get install -y ${DEPENDS} }

二、提供安装类bbclass

我们知道yocto分如下步骤:

image.png 对于此,我们需要将此流程定制,inherit 我们自己的kylin.bbclass,如下:

  • do_fetch : 设置为noexec
  • do_unpack: 设置为noexec
  • do_patch: 设置为noexec
  • do_configure:设置为noexec
  • do_compile:设置为noexec
  • do_install: 集成kylin的bbclass,实现chroot到系统中,apt-get install 的方式安装
  • do_package:设置为noexec

三、设置bb file

在我们的layer中的bb file,需要inherit kylindeb这样的类,这样默认就指向了我们自己的流程

我们在do_install中会进入chroot环境中进行apt-get 安装包,这里bb file需要提供一个packages-list文件,用于解析packages-list里面的包列表,用于安装,如下示例:

PACKAGE_LIST = "ukui-tablet-desktop ukui-control-center ukui-menu" do_install() { apt-get update apt-get install -y ${PACKAGE_LIST} }

四、构建recipes

最后我们通过命令即可构建

bitback kylin

至此,yocto可以具备通过安装二进制的方式来构建系统环境。