lab2–01
正式进入lab2之前需要一些铺垫知识
铺垫知识----数据段、代码段、堆栈段、BSS段的区别
linxu中段的管理
在Linux下内存分配是以页为单位的,而页是通过段管理,各个段之间是独立的,方便管理。一个简单的程序被编译成目标文件后的结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uhfsog1a-1617863705778)(https://ivanzz1001.github.io/records/assets/img/cplusplus/cpp_segment_example.jpg)]
从上图可以看出,已经初始化的全局变量int global_init_var=84;
和已经初始化的局部静态变量static int static_var=85;
保存在了.data
段,未初始化的全局变量和未初始化的局部变量保存在了.bss
段。
目标文件各个段在文件中的布局如下:
现在开始说说上图各个段
- init段
程序初始化的入口代码,在
main()
之前运行。解释一下,main()
是主函数,进入main()
就意味着初始化已经结束,开始运行了。
- bss段
bss段属于静态内存分配。通常用来存放程序中未初始化的全局变量和未初始化的局部静态变量。未初始化的全局变量一般会在函数体(包括
main()
)之外,作为全局的未初始化的变量,默认值是0;未初始化的局部静态变量,一般会有static
类型。
在程序运行时,才会给bss段里面的变量分配内存空间。
section table
中保存了bss段内存空间大小总和。可以通过objdump -h *.o
查看
- data段
数据段存放已经初始化的全局变量和已经初始化的静态变量的一块内存区域。数据段属于静态内存分配。
- text段
代码段(code segment / text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
- rodata
存放的是只读数据,比如字符串常量,全局
const
变量和#define
定义的常量。本段又称为常量区。例如:
char *p = "123456";
这里的123456
就存放在rodata
段中。
- strtab段
存储的是变量名、函数名等。例如:
char *szPath = "/root";
void func();
上述变量名szPath
和函数名func
存储在strtab段里。
- rel.text段
针对text段的重定位表,还有rel.data(针对data段的重定位表)
- heap堆
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc()等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free()等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
- stack栈
是用户存放程序临时创建的局部变量,也就是说我们函数括弧{}中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把栈看成一个寄存、交换临时数据的内存区。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D834NzbS-1617863705782)(https://ivanzz1001.github.io/records/assets/img/cplusplus/cpp_memory_layout.jpg)]
举例
示例代码
#include <stdio.h>
int a_text;
const int b_test = 2;
static int c_test = 3;
static int d_test;
int e_test = 0;
char *p_test = "hello,world";
const char *q_test = "good";
int main(int argc, char *argv[]){
static int m_test;
static int n_test = 0;
char *s_test = "just for test";
printf("a:%d\n", a_test);
printf("b: %d\n", b_test);
printf("c: %d\n", c_test);
printf("d: %d\n", d_test);
printf("e: %d\n", e_test);
printf("p: %s\n", p_test);
printf("q: %s\n", q_test);
printf("m: %d\n", m_test);
printf("n: %d\n", n_test);
printf("s: %s\n", s_test);
return 0x0;
}
通过下面的命令逐步编译运行上面test.c
程序:
# gcc -E -o test.s test.c ; 预处理
# gcc -S -o test.s test.c ;编译为汇编代码
# gcc -c -o test.o test.s ;编译为目标文件test.o
# gcc -o test test.o ;链接,生成可执行文件test
# ls
test test.c test.o test.s
查看test可执行文件的布局
# objdump -h test
test: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 00000013 08048154 08048154 00000154 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 08048168 08048168 00000168 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 08048188 08048188 00000188 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .gnu.hash 00000020 080481ac 080481ac 000001ac 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .dynsym 00000050 080481cc 080481cc 000001cc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynstr 0000004c 0804821c 0804821c 0000021c 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .gnu.version 0000000a 08048268 08048268 00000268 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version_r 00000020 08048274 08048274 00000274 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .rel.dyn 00000008 08048294 08048294 00000294 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rel.plt 00000010 0804829c 0804829c 0000029c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .init 00000023 080482ac 080482ac 000002ac 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .plt 00000030 080482d0 080482d0 000002d0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt.got 00000008 08048300 08048300 00000300 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .text 00000262 08048310 08048310 00000310 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .fini 00000014 08048574 08048574 00000574 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .rodata 00000071 08048588 08048588 00000588 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 .eh_frame_hdr 0000002c 080485fc 080485fc 000005fc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 .eh_frame 000000cc 08048628 08048628 00000628 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .init_array 00000004 08049f08 08049f08 00000f08 2**2
CONTENTS, ALLOC, LOAD, DATA
19 .fini_array 00000004 08049f0c 08049f0c 00000f0c 2**2
CONTENTS, ALLOC, LOAD, DATA
20 .jcr 00000004 08049f10 08049f10 00000f10 2**2
CONTENTS, ALLOC, LOAD, DATA
21 .dynamic 000000e8 08049f14 08049f14 00000f14 2**2
CONTENTS, ALLOC, LOAD, DATA
22 .got 00000004 08049ffc 08049ffc 00000ffc 2**2
CONTENTS, ALLOC, LOAD, DATA
23 .got.plt 00000014 0804a000 0804a000 00001000 2**2
CONTENTS, ALLOC, LOAD, DATA
24 .data 00000014 0804a014 0804a014 00001014 2**2
CONTENTS, ALLOC, LOAD, DATA
25 .bss 00000018 0804a028 0804a028 00001028 2**2
ALLOC
26 .comment 00000035 00000000 00000000 00001028 2**0
CONTENTS, READONLY
- 1) VMA和LMA
我们先简要介绍一下VMA
和LMA
这两个字段:
VMA
(virtual memory address): 程序区段在执行时期的地址
LMA
(load memory address): 某程序区段加载时的地址。因为我们知道程序运行前要经过:编译、链接、装载、运行等过程。装载到哪里呢? 没错,就是LMA对应的地址里。
一般情况下,LMA
和VMA
都是相等的,不等的情况主要发生在一些嵌入式系统上。
- 2) segment布局
如下我们简要的画出各区段的一个布局:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p8zfoQQP-1617863705783)(https://ivanzz1001.github.io/records/assets/img/cplusplus/cpp_lma_vma.jpg)]
其实这里我们看到尽管bss段长度为0x00000018,但是我们看到.bss段和.comment段在文件中的偏移是一样的,这就说明bss段数据是不用包含在可执行程序中的。
汇编文件test.s文件
# cat test.s
.file "test.c"
.comm a_test,4,4
.globl b_test
.section .rodata
.align 4
.type b_test, @object
.size b_test, 4
b_test:
.long 2
.data
.align 4
.type c_test, @object
.size c_test, 4
c_test:
.long 3
.local d_test
.comm d_test,4,4
.globl e_test
.bss
.align 4
.type e_test, @object
.size e_test, 4
e_test:
.zero 4
.globl p_test
.section .rodata
.LC0:
.string "hello,world"
.data
.align 4
.type p_test, @object
.size p_test, 4
p_test:
.long .LC0
.globl q_test
.section .rodata
.LC1:
.string "good"
.data
.align 4
.type q_test, @object
.size q_test, 4
q_test:
.long .LC1
.section .rodata
.LC2:
.string "just for test"
.LC3:
.string "a: %d\n"
.LC4:
.string "b: %d\n"
.LC5:
.string "c: %d\n"
.LC6:
.string "d: %d\n"
.LC7:
.string "e: %d\n"
.LC8:
.string "p: %s\n"
.LC9:
.string "q: %s\n"
.LC10:
.string "m: %d\n"
.LC11:
.string "n: %d\n"
.LC12:
.string "s: %s\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
leal 4(%esp), %ecx
.cfi_def_cfa 1, 0
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
.cfi_escape 0x10,0x5,0x2,0x75,0
movl %esp, %ebp
pushl %ecx
.cfi_escape 0xf,0x3,0x75,0x7c,0x6
subl $20, %esp
movl $.LC2, -12(%ebp)
movl a_test, %eax
subl $8, %esp
pushl %eax
pushl $.LC3
call printf
addl $16, %esp
movl $2, %eax
subl $8, %esp
pushl %eax
pushl $.LC4
call printf
addl $16, %esp
movl c_test, %eax
subl $8, %esp
pushl %eax
pushl $.LC5
call printf
addl $16, %esp
movl d_test, %eax
subl $8, %esp
pushl %eax
pushl $.LC6
call printf
addl $16, %esp
movl e_test, %eax
subl $8, %esp
pushl %eax
pushl $.LC7
call printf
addl $16, %esp
movl p_test, %eax
subl $8, %esp
pushl %eax
pushl $.LC8
call printf
addl $16, %esp
movl q_test, %eax
subl $8, %esp
pushl %eax
pushl $.LC9
call printf
addl $16, %esp
movl m_test.1942, %eax
subl $8, %esp
pushl %eax
pushl $.LC10
call printf
addl $16, %esp
movl n_test.1943, %eax
subl $8, %esp
pushl %eax
pushl $.LC11
call printf
addl $16, %esp
subl $8, %esp
pushl -12(%ebp)
pushl $.LC12
call printf
addl $16, %esp
movl $0, %eax
movl -4(%ebp), %ecx
.cfi_def_cfa 1, 0
leave
.cfi_restore 5
leal -4(%ecx), %esp
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.local m_test.1942
.comm m_test.1942,4,4
.local n_test.1943
.comm n_test.1943,4,4
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
在讲解各变量存放位置之前,我们先简要介绍一下.comm
和.lcomm
:
-
comm
: 声明为未初始化的通用内存区域; -
lcomm
: 声明为未初始化的本地内存区域;
其实上述两个均是属于bss段,不过作用域范围有些不同。.lcomm
是为不会从本地汇编代码之外进行访问的数据保留的。两者使用的格式为:
.comm symbol, length
.lcomm symbol, length
例如:
.section .bss
.lcomm buffer, 1000
该语句把1000字节的内存地址赋予标签buffer,在声明本地通用内存区域的程序之外的函数是不能访问他们的。
在bss段声明的好处是,数据不包含在可执行文件中。在数据段中定义数据时,它必须被包含在可执行程序中,因为必须使用特定值初始化它。因为不使用数据初始化bss段中声明的数据区域,所以内存区域被保留在运行时使用,并且不必包含在最终的程序中。
致谢:
nasm的使用
创建"hello.asm"
文件:
touch hello.asm ;使用touch命令创建一个名为hello.asm的空文件
gedit hello.asm ;使用gedit命令编辑hello.asm
在文件中输入下面的汇编代码
section .data
hello: db 'Hello world!',10 ; 'Hello world!' plus a linefeed character
helloLen: equ $-hello ; Length of the 'Hello world!' string
; (I'll explain soon)
section .text
global _start
_start:
mov eax,4 ; The system call for write (sys_write)
mov ebx,1 ; File descriptor 1 - standard output
mov ecx,hello ; Put the offset of hello in ecx
mov edx,helloLen ; helloLen is a constant, so we don't need to say
; mov edx,[helloLen] to get it's actual value
int 80h ; Call the kernel
mov eax,1 ; The system call for exit (sys_exit)
mov ebx,0 ; Exit with return code of 0 (no error)
int 80h
保存后退出,编译
nasm -f elf64 hello.asm
如果是32位系统就把elf64
改为elf32
链接
ld -s -o hello hello.o
运行
./hello
终端输出"hello,world!"
就没有问题了
致谢:
shellcode—arm
- as 汇编程序
- strace 跟踪系统调用的实用程序
- objdump 用于检查反汇编中的空字节
- objcopy 从ELF二进制文件中提取原始shellcode
基本原则 -
- 让我们的shellcode保持紧凑,不要有空字节;原因:我们编写shellcode的目的是它来利用缓冲区溢出等内存损坏漏洞。例如再一些
strcpy
函数造成的缓冲区漏洞中,strcpy()
的工作是复制数据,在收到空字节后停止复制。当我们使用这个溢出来控制程序流时,如果strcpy
命中空字节,它将停止复制shellcode
,我们的利用就不会起到作用。
- 让我们的shellcode保持紧凑,不要有空字节;原因:我们编写shellcode的目的是它来利用缓冲区溢出等内存损坏漏洞。例如再一些
-
- 避免库函数调用和绝对内存地址;原因:为了使我们的
shellcode
尽可能通用,我们不能依赖需要特定依赖关系的库调用和依赖特定环境的绝对内存地址。
编写shellcode的过程包含一下步骤:
- 避免库函数调用和绝对内存地址;原因:为了使我们的
1、了解要使用的系统调用
2、找出系统调用号码和选择系统调用函数所需的参数
3、使shellcode有效化
4、将shellcode转化为Hex字符串
理解系统调用
在深入研究第一个shellcode之前,让我们来编写一个能输出字符串的简单ARM汇编程序。 第一步是查找我们想要使用的系统调用,这个时候应该使用“write”。
size_t write(int fd, const void *buf, size_t count);
从像C这样的高级编程语言的角度来看,这个系统调用示例如下:
const char string[13] = "Azeria Labs\n";
write(1,string,sizeof(string));
从这个原型中可以看到我们需要的参数:
- fd 标准输出STDOUT,为1
- buf 指向字符串的指针
- count 要写入的字节数,这里是13
- 要写入的系统调用数-> 0x4
对于前3个参数,可以使用R0,R1和R2。 对于系统调用,我们需要使用R7并将0x4移入其中。
mov r0, #1 ;fd 1 = STDOUT
ldr r1, string ;loading the string from memory to R1
mov r2, #13 ;write 13 bytes to STDOUT
mov r7, #4 ;Syscall 0x4 = write()
svc #0
通过使用以上的代码片段,ARM组装程序如下:
.data
string: .asciz "Azera Labs\n"
after_string:
.set size_of_string, after_string - string
.text
.global _start
_start:
mov r0, #1 ;fd 1 = STDOUT
ldr r1, string ;loading the string from memory to R1
mov r2, #13 ;write 13 bytes to STDOUT
mov r7, #4 ;Syscall 0x4 = write()
swi #0. ;invoke syscall
_exit:
mov r7, #1. ;exit syscall
swi 0. ;invoke syscall
addr_of_string: .word string
在数据部分,我们通过从字符串后面的地址减去字符串开头的地址来计算字符串的大小。 当然,如果我们可以手动计算字符串大小并将结果直接放入R2中,则无需这样做。另外,使用系统调用号为1的exit()来退出程序。
编译并执行:
azeria@labs:~$ as write.s -o write.o && ld write.o -o write
azeria@labs:~$ ./write
Azeria Labs
酷。 现在我们已经了解了这个过程,接下来让我们更详细地研究它,并在ARM程序集中编写一个简单的shellcode。
跟踪系统调用
对于我们的第一个例子,我们将采用以下简单函数并将其转换为ARM程序集:
#include <stdio.h>
void main(void)
{
system("/bin/sh");
}
第一步是干什么?第一步是确定此函数需要的系统调用以及系统调用所需的参数。可以使用strace
对OS内核的系统调用进行跟踪。
将上面的代码保存在文件中,然后在运行strace
命令编辑它
azeria@labs:~$ gcc system.c -o system
azeria@labs:~$ strace -h
-f -- follow forks, -ff -- with output into separate files
-v -- verbose mode: print unabbreviated argv, stat, termio[s], etc. args
--- snip --
azeria@labs:~$ strace -f -v system
--- snip --
[pid 4575] execve("/bin/sh", ["/bin/sh"], ["MAIL=/var/mail/pi", "SSH_CLIENT=192.168.200.1 42616 2"..., "USER=pi", "SHLVL=1", "OLDPWD=/home/azeria", "HOME=/home/azeria", "XDG_SESSION_COOKIE=34069147acf8a"..., "SSH_TTY=/dev/pts/1", "LOGNAME=pi", "_=/usr/bin/strace", "TERM=xterm", "PATH=/usr/local/sbin:/usr/local/"..., "LANG=en_US.UTF-8", "LS_COLORS=rs=0:di=01;34:ln=01;36"..., "SHELL=/bin/bash", "EGG=AAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., "LC_ALL=en_US.UTF-8", "PWD=/home/azeria/", "SSH_CONNECTION=192.168.200.1 426"...]) = 0
--- snip --
[pid 4575] write(2, "$ ", 2$ ) = 2
[pid 4575] read(0, exit
--- snip --
exit_group(0) = ?
+++ exited with 0 +++
结果证明,系统函数execve()
正在被调用
系统调用编码和参数
下一步是找出execve()的系统调用编号和所需的参数。 您可以通过w3calls或Linux手册页查找系统调用的概述。 这是我们从execve()的手册页中得到的:
NNME
execve - execve program
SYNOPSIS
#include <unostd.h>
int execve(const char *filename, char *constargv[], char *const envp[]);
execve()要求的参数:
- 指向指定二进制路径的字符串的指针
- argv[] 命令行变量数组
- envp[] 环境变量数组
这些基本上可以转换为:execve(filename,argv[],envp[])->execve(filename,0,0)
.使用以下命令查找此函数的系统调用编号。
azeria@labs:~$ grep execve /usr/include/arm-linux-gnueabihf/asm/unistd.h
#define __NR_execve (__NR_syscall_BASE +11)
查看输出,可以看到execve()的系统调用编号是11。寄存器R0到R2可用于函数参数,而寄存器R7可以存储系统调用编号。
在x86上调用系统调用的工作方式如下:首先,将参数入栈。 然后,系统调用编号被移入EAX寄存器( MOV EAX,syscall_number)。 最后,使用SYSENTER/INT 80调用系统调用。
在ARM上,系统调用的工作方式略有不同:
1.将参数移动到寄存器 - R0,R1,…
2.将系统调用编号移动到寄存器R7中
mov r7,#<syscall_number></syscall_number>
3.产生一个系统调用
SVC#0
SVC#1
4.返回值存入R0
这是它在ARM Assembly中的样子
正如您在上图中所看到的,我们首先使用PC相对寻址将R0指向我们的“/bin/sh”字符串。 然后我们将0移动到R1和R2并将系统调用编号11移动到R7。 看起来很简单吧? 让我们看一下使用objdump的第一次尝试的反汇编:
azeria@labs:~$ as execve1.s -o execve1.o
azeria@labs:~$ objdump -d execve1.o
execve1.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
0: e28f000c add r0, pc, #12
4: e3a01000 mov r1, #0
8: e3a02000 mov r2, #0
c: e3a0700b mov r7, #11
10: ef000000 svc 0x00000000
14: 6e69622f .word 0x6e69622f
18: 0068732f .word 0x0068732f
事实证明我们的shellcode中有很多空字节。 下一步是使shellcode有效化,并替换所有涉及的操作。
使shellcode有效化
Thumb模式是减小空字节出现在我们的shellcode中的几率的可用技术之一。这是因为Thumb指令长度为2个字节而不是4个。如果您完成了ARM Assembly Basics教程,就会知道如何从ARM切换到Thumb模式。 如果还没有,我建议您阅读“条件执行和分支”教程的第6部分中有关分支指令“B / BX / BLX”的章节。
在我们的第二次尝试中,我们使用Thumb模式并将包含#0的操作替换为导致0的操作,具体方法是相互减去寄存器或进行异或操作。 例如,不使用“mov r1,#0”
,而是使用“sub r1, r1, r1”(r1 = r1-r1)
或“eor r1,r1,r1”(r1 = r1 xor r1)
。 请记住,由于我们现在使用Thumb模式(2字节指令),我们的代码必须是4字节对齐,并且我们需要在末尾添加NOP(例如mov r5,r5)
。
反汇编的代码如下:
结果是我们只需要摆脱一个空字节。 我们的代码中导致空字节的部分是以空字符结尾的字符串“/bin/sh\ 0”。 我们可以使用以下技术解决此问题:
- 将“/bin/sh\0”替换为“/bin/shX”
- 将指令strb(存储字节)与现有的零填充寄存器结合使用,将X替换为空字节
如图:
这样就没有空字节了。
将shellcode转换为hex string
现在我们创建的shellcode可以转换为它的十六进制表示。 在此之前,最好检查shellcode是否能独立工作。 但是有一个问题:如果我们像通常那样编译汇编文件,它将无法工作。 因为我们用了strb操作来修改代码段(.text)。 这要求代码段是可写的,可以通过在链接过程中添加-N标志来实现。
azeria@labs:~$ ld --help
--- snip --
-N, --omagic Do not page align data, do not make text readonly.
--- snip --
azeria@labs:~$ as execve3.s -o execve3.o && ld -N execve3.o -o execve3
azeria@labs:~$ ./execve3
$ whoami
azeria
有用! 恭喜,您已经在ARM程序集中编写了第一个shellcode。
要将其转换为十六进制,请使用以下指令:
azeria@labs:~$ objcopy -O binary execve3 execve3.bin
azeria@labs:~$ hexdump -v -e '"\\""x" 1/1 "%02x" ""' execve3.bin
\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68\x78
也可以使用简单的python脚本执行相同操作,而不是使用上面的hexdump指令:
#!/usr/bin/env python
import sys
binary = open(sys.argv[1],'rb')
for byte in binary.read():
sys.stdout.write("\\x"+byte.encode("hex"))
print ""
azeria@labs:~$ ./shellcode.py execve3.bin
\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68\x78
致谢:
如何用x86编写shellcode
致谢:
shellcode编写总结
致谢:
- shellcode编写过程总结 还得看,没懂
pwntools的简单介绍1
致谢:
- pwntools的简单介绍
首先导入包
from pwn import*
你将在全局空间里引用pwntools的所有函数。现在可以用一些简单函数进行汇编,反汇编,pack,unpack等等操作。
将包导入后,我一般都会设置日志记录级别,方便出现问题的时候排查错误
context.log_level = 'debug'
这样设置后,通过管道发送和接收的数据都会被打印在屏幕上。
然后就是连接了,一般题目都会给你一个ip和一个端口,让你用nc连接访问,也有的题是让你通过ssh连接,这两种方式都可以通过pwntools实现。
# 第一种连接方式,通过ip和port去连接
conn = remote('127.0.0.1', 8888)
# 第二种连接方式,通过ssh连接
shell = ssh(host='192.168.14.144', user='root', port=2222, password='123456')
在编写exp时,最常见的工作就是在整数之间转换,而且转换后,它们的表现形式就是一个字节序列,pwntools提供了打包函数。
# 比如将0xdeadbeef进行32位的打包,将会得到'\xef\xbe\xad\xde'(小端序)
payload = p32(0xdeadbeef) #pack 32 bits number
payload = p64(0xdeadbeef) #pack 64 bits number
打包的时候要指定程序是32位还是64位的,他们之间打包后的长度是不同的。
建立连接后就可以发送和接收数据了。
conn.send(data) #发送数据
conn.sendline(data) #发送一行数据,相当于在数据后面加\n
#接收数据,numb制定接收的字节,timeout指定超时
conn.recv(numb = 2048, timeout = default)
#接受一行数据,keepends为是否保留行尾的\n
conn.recvline(keepends=True)
#接受数据直到我们设置的标志出现
conn.recvuntil("Hello,World\n",drop=fasle)
conn.recvall() #一直接收直到 EOF
conn.recvrepeat(timeout = default) #持续接受直到EOF或timeout
#直接进行交互,相当于回到shell的模式,在取得shell之后使用
conn.interactive()
做过pwn题的人应该知道,如果是通过nc连接的话,一进去就相当于开启了一个进程,你只需要发送数据和程序交互就行了。
如果是通过ssh连接进去的,你需要手动创建一个进程,就跟正常通过ssh连接一样,你需要打开一个程序。
比如:
>>> sh = process('/bin/sh') # 创建进程,这里开了一个shell
>>> sh.sendline('sleep 3; echo hello world;') # 发送命令
>>> sh.recvline(timeout=1) # 接收数据
''
>>> sh.recvline(timeout=5)
'hello world\n'
>>> sh.close() # 关闭进程
ELF模块用于获取ELF文件的信息,首先使用ELF()获取这个文件的句柄,然后使用这个句柄调用函数,和IO模块很相似。
下面演示了:获取基地址、获取函数地址(基于符号)、获取函数got地址、获取函数plt地址
>>> e = ELF('/bin/cat')
>>> print hex(e.address) # 文件装载的基地址
0x400000
>>> print hex(e.symbols['write']) # 函数地址
0x401680
>>> print hex(e.got['write']) # GOT表的地址,GOT表为全局函数表
0x60b070
>>> print hex(e.plt['write']) # PLT的地址,PLT表为内部函数表
0x401680
致谢:GOT表和PLT表知识详解
汇编与反汇编
>>> asm('mov eax, 0') #汇编
'\xb8\x00\x00\x00\x00'
>>> disasm('\xb8\x0b\x00\x00\x00') #反汇编
' 0: b8 0b 00 00 00 mov eax,0xb'
pwnlib.shellcraft模块包含生成shell代码的函数。
其中的子模块声明结构,比如
- ARM架构: shellcraft.arm
- AMD64架构: shellcraft.amd64
- Intel 80386架构: shellcraft.i386
- 通用: shellcraft.common
可以通过context设置架构,然后生成shellcode
context(arch='i386', os='linux')
shellcode = asm(shellcraft.sh())
调用gdb调试在python文件中直接设置断点,当运行到该位置之后就会断下
from pwn import *
p = process('./c')
gdb.attach(p)