
0. Go
Go는 빠르고 병렬 프로세싱을 구현하기 쉬우며 크로스 컴파일링이 가능해 어플리케이션 개발에 인기 있는 언어디다.
Zebrocy, WellMess, Mirai의 C2서버는 Go로 구현되었으며 최근 멀웨어는 Go로 쓰인 경우도 있어 알아둬야한다.
1. level4 crackme(Go)
level 4 크랙미는 go 언어로 구현된 바이너리를 분석해야한다.
file 명령어에서 Go Build ID를 보아 이 파일이 go 언어로 쓰인 것을 알 수 있다.

파일을 실행하면 Access code를 입력하고 코드가 다르면 Bad Code가 나온다.

2. Go 바이너리 배경지식
C/C++로 컴파일하면 엔트리 포인트가 Winmain이나 main이지만 Go는 _rt0_amd64_linux나 _rt0_386_window처럼 rt0_아키텍처이름_os이름 형식으로 엔트리 포인트이름이 된다.main함수는 main.main함수에 구현된다.
Go는 함수의 인수와 반환값을 스택을 통해서만 전달하는 독특한 방식을 사용했다. 하지만 Go 1.17 버전부터는 성능 최적화를 위해 레지스터 기반(Register-based ABI) 호출 규약을 도입하여, 현재는 레지스터를 우선 사용하고 부족할 경우 스택을 사용한다.
Go 바이너리는 내부 함수가 매우 많고 용량이 큰 편이다. 이는 실행에 필요한Go 런타임(Garbage Collector, 스케줄러 등)이 바이너리에 정적으로 모두 포함된다. 파이썬이 실행을 위해 별도의 인터프리터(python.dll 등)를 필요로 하는 것과 달리, Go는 실행 환경 자체를 기계어로 컴파일하여 내장하므로 별도의 의존성 없이 단독 실행이 가능하며 최적화되어 있다.
또한 Go 바이너리는 리플렉션과 패닉 시 스택 추적을 위해 .gopclntab 섹션에 심벌 정보를 저장한다. 리버스 엔지니어링 시 이 섹션을 분석하면 함수 이름과 소스 코드 라인 정보 등을 높은 수준으로 복구할 수 있다.
- 리플렉션: 실행 중에 데이터의 타입과 구조를 파악해야 하므로, 타입 이름과 구조 정보가 바이너리에 저장된다.
- 패닉: 프로그램이 비정상 종료될 때 정확한 에러 위치(파일, 라인, 함수명)를 알려줘야 하므로, 소스 코드 위치 정보(.gopclntab)가 바이너리에 저장된다.
3. 심벌 복원 및 호출 규약 맞추기
아래는 level4 go 언어로 작성된 코드의 디컴파일 결과이다. go는 앞에서 심벌 정보를 저장한다.
심볼 정보를 사용해 디컴파일된 함수 이름을 매핑해보자.
puStack_18 = &stack0x00000008;
DAT_00575228 = auStack_30;
DAT_00575220 = auStack_ffc8;
piVar1 = (int *)cpuid_basic_info(0);
iVar3 = piVar1[2];
iVar4 = piVar1[3];
if (*piVar1 != 0) {
if (((piVar1[1] == 0x756e6547) && (iVar3 == 0x49656e69)) && (iVar4 == 0x6c65746e)) {
DAT_0059e14c = 1;
DAT_0059e150 = 1;
puVar2 = (undefined4 *)cpuid_Version_info(1);
_DAT_0059e1a0 = *puVar2;
iVar3 = puVar2[2];
iVar4 = puVar2[3];
DAT_00575230 = DAT_00575220;
DAT_00575238 = DAT_00575220;
uStack_20 = unaff_retaddr;
if (DAT_005746c8 == (code *)0x0) {
ppuStack_38 = (undefined **)0x4598e0;
FUN_0045dc00(&DAT_00575808,*piVar1,iVar3,iVar4);
*(undefined8 *)(in_FS_OFFSET + -8) = 0x123;
if (DAT_00575808 != 0x123) {
ppuStack_38 = (undefined **)0x459901;
FUN_0045b310();
ppuStack_38 = (undefined **)0x4598ba;
DAT_00575228 = auStack_30;
(*DAT_005746c8)(&DAT_00575220,&LAB_0045b300,0,0);
DAT_00575230 = DAT_00575220 + 0x380;
DAT_00575238 = DAT_00575230;
*(undefined1 ***)(in_FS_OFFSET + -8) = &DAT_00575220;
DAT_00575780 = &DAT_00575220;
DAT_00575250 = &DAT_00575780;
ppuStack_38 = (undefined **)0x459925;
FUN_0043e8f0();
auStack_30[0] = uStack_20;
puStack_28 = puStack_18;
ppuStack_38 = (undefined **)0x45993b;
FUN_0043e390();
ppuStack_38 = (undefined **)0x459940;
FUN_0042bfb0();
ppuStack_38 = (undefined **)0x459945;
FUN_00432320();
ppuStack_38 = &PTR_LAB_004e4b80;
uStack_40 = 0;
uStack_48 = 0x459954;
FUN_00439010();
ppuStack_38 = (undefined **)0x45995b;
FUN_00433a70();
ppuStack_38 = (undefined **)0x459960;
FUN_0045b310();
return;
심볼 정보를 매핑하기 위해서 기드라 스크립트를 사용한다.
아래 깃허브에서 renamer를 다운받아서 실행하면
https://github.com/ghidraninja/ghidra_scripts/blob/master/golang_renamer.py
ghidra_scripts/golang_renamer.py at master · ghidraninja/ghidra_scripts
Scripts for the Ghidra software reverse engineering suite. - ghidraninja/ghidra_scripts
github.com
함수가 매핑된다.


main.main 소스코드는 아래와 같다, go옛날 버전에서 컴파일된 버전은 스택을 통해서만 할당되는데 기드라에서는 잘 파악하지 못해 기드라 플러그인을 사용해야한다.
void main_main_49A040(void)
{
while (local_f8 <= *(undefined1 **)(*(long *)(in_FS_OFFSET + -8) + 0x10)) {
runtime_morestack_noctxt_459BB0();
}
local_a8 = &DAT_004a9080;
ppuStack_a0 = &PTR_DAT_004e4fc0;
fmt_Fprint_490AA0();
FUN_0045c51a(0,&local_88);
local_80 = FUN_0045c51a(&local_88);
local_88 = &PTR_DAT_004e6840;
local_78 = &PTR_bufio_ScanLines_46C040_004d18c0;
local_70 = 0x10000;
bufio__ptr_Scanner_Scan_46B670();
runtime_slicebytetostring_447810();
local_d0 = 1;
math_rand__ptr_Rand_Seed_498F70();
math_rand__ptr_Rand_Intn_499360();
strconv_FormatInt_470810();
runtime_stringtoslicebyte_4479B0();
crypto_sha256_Sum256_483D60();
local_f0 = local_58;
uStack_e8 = 1;
local_98 = 0;
uStack_90 = 0;
runtime_convT2Enoptr_409280();
uStack_90 = local_58;
fmt_Sprintf_4909B0();
if (lStack_148 == local_150) {
runtime_memequal_4020E0();
local_b8 = &DAT_004a9080;
ppuStack_b0 = &PTR_DAT_004e4fd0;
fmt_Fprintln_490BA0();
}
else {
local_c8 = &DAT_004a9080;
ppuStack_c0 = &PTR_DAT_004e4fe0;
fmt_Fprintln_490BA0();
}
return;
}
기드라 플러그인은 https://github.com/mooncat-greenpy/Ghidra_GolangAnalyzerExtension
GitHub - mooncat-greenpy/Ghidra_GolangAnalyzerExtension: Analyze Golang with Ghidra
Analyze Golang with Ghidra. Contribute to mooncat-greenpy/Ghidra_GolangAnalyzerExtension development by creating an account on GitHub.
github.com
플러그인을 다운받아 실행하면 함수에 인자가 잘 인식되는 것을 볼 수 있다.
/* /home/kotake/code/book_crackme/level4.go:12 */
while (local_f8 <= *(undefined1 **)(*(int *)(in_FS_OFFSET + -8) + 0x10)) {
/* /home/kotake/code/book_crackme/level4.go:12 */
runtime_morestack_noctxt_459BB0();
}
/* /home/kotake/code/book_crackme/level4.go:13 */
local_a8 = &datatype.String.string;
ppuStack_a0 = &goss_Access_code:__4e4fc0;
/* /usr/lib/go-1.14/src/fmt/print.go:242 */
uVar9 = 1;
uVar6 = DAT_00574718;
fmt_Fprint_490AA0(&PTR_datatype.Interface.io.Writer_004e6860,DAT_00574718,&local_a8,1,1,
in_stack_fffffffffffffeb0,in_stack_fffffffffffffeb8,in_stack_fffffffffffffec0);
/* /home/kotake/code/book_crackme/level4.go:14 */
/* /usr/lib/go-1.14/src/bufio/scan.go:87 */
uVar2 = 0;
uVar3 = 0;
uVar4 = 0;
uVar5 = 0;
runtime.duffzero_0x0_0x80_FUN_0045c51a((undefined1 *)&local_88,(uint16)ZEXT816(0));
auVar1._4_4_ = uVar3;
auVar1._0_4_ = uVar2;
auVar1._8_4_ = uVar4;
auVar1._12_4_ = uVar5;
runtime.duffzero_0x0_0x80_FUN_0045c51a((undefined1 *)&local_88,(uint16)auVar1);
local_88.r.tab = (runtime._type *)&PTR_datatype.Interface.io.Reader_004e6840;
local_88.split = &PTR_bufio_ScanLines_46C040_004d18c0;
local_88.maxTokenSize = 0x10000;
/* /home/kotake/code/book_crackme/level4.go:15 */
bufio__ptr_Scanner_Scan_46B670(&local_88,uVar6);
/* /home/kotake/code/book_crackme/level4.go:16 */
/* /usr/lib/go-1.14/src/bufio/scan.go:112 */
runtime_slicebytetostring_447810
(local_110,local_88.token.__values,local_88.token.__count,local_88.token.__capacity,
uVar9,in_stack_fffffffffffffeb0);
iVar7 = in_stack_fffffffffffffeb0;
local_d0 = uVar9;
/* /home/kotake/code/book_crackme/level4.go:17 */
/* /usr/lib/go-1.14/src/bufio/scan.go:112 */
/* /usr/lib/go-1.14/src/math/rand/rand.go:303 */
math_rand__ptr_Rand_Seed_498F70(DAT_00574700,0x2e0ce0);
/* /usr/lib/go-1.14/src/math/rand/rand.go:337 */
math_rand__ptr_Rand_Intn_499360(DAT_00574700,0xe05def0d10,local_88.token.__count);
/* /usr/lib/go-1.14/src/strconv/itoa.go:35 */
strconv_FormatInt_470810
(local_88.token.__count,10,local_88.token.__count,local_88.token.__capacity);
/* /home/kotake/code/book_crackme/level4.go:18 */
runtime_stringtoslicebyte_4479B0
(local_130,local_88.token.__count,local_88.token.__capacity,local_88.token.__capacity,
uVar9,iVar7);
iVar10 = iVar7;
crypto_sha256_Sum256_483D60
(local_88.token.__capacity,uVar9,iVar7,local_88.token.__capacity,uVar9,iVar7,
in_stack_fffffffffffffeb8);
/* /home/kotake/code/book_crackme/level4.go:19 */
local_98 = 0;
uStack_90 = 0;
uStack_e8 = uVar9;
runtime_convT2Enoptr_409280(&datatype.Array.[32]uint8,local_f0,iVar7,local_88.token.__capacity);
cVar8 = '\x01';
fmt_Sprintf_4909B0("%x",2,&local_98,1,1,iVar10,in_stack_fffffffffffffeb8);
/* /home/kotake/code/book_crackme/level4.go:20 */
/* /home/kotake/code/book_crackme/level4.go:20 */
if ((in_stack_fffffffffffffeb8 == in_stack_fffffffffffffeb0) &&
(runtime_memequal_4020E0(iVar10,local_d0,in_stack_fffffffffffffeb8,cVar8), cVar8 != '\0')) {
/* /home/kotake/code/book_crackme/level4.go:21 */
local_b8 = &datatype.String.string;
ppuStack_b0 = &goss_Good_Job!!!_4e4fd0;
/* /usr/lib/go-1.14/src/fmt/print.go:274 */
fmt_Fprintln_490BA0(&PTR_datatype.Interface.io.Writer_004e6860,DAT_00574718,&local_b8,1,1,iVar10
,in_stack_fffffffffffffeb8,in_stack_fffffffffffffec0);
return;
/* /home/kotake/code/book_crackme/level4.go:21 */
}
/* /home/kotake/code/book_crackme/level4.go:23 */
local_c8 = &datatype.String.string;
ppuStack_c0 = &goss_Bad_Code..._4e4fe0;
/* /usr/lib/go-1.14/src/fmt/print.go:274 */
fmt_Fprintln_490BA0(&PTR_datatype.Interface.io.Writer_004e6860,DAT_00574718,&local_c8,1,1,iVar10,
in_stack_fffffffffffffeb8,in_stack_fffffffffffffec0);
return;
}
4. 분석
go 함수 프롤로그
이 코드는 Go 언어의 가장 큰 특징 중 하나인 동적 스택 확장(Stack Growth/Stack Splitting)이다.
현재 스택 포인터(SP, 여기서는 local_f8로 표현)가 한계선보다 작은지 검사해 작으면 runtime_morestack_noctxt함수를 실행한다.

fmt_Fprint(): Access Code를 출력한다.

Scanner_Scan(): 입력 받는다.

시드 생성(시드 값이 0x2e0ce0 = 3017952 로 고정 되어 있고 0~963666283808 사이의 값을 뽑는다.)

sha256생성 후 포멧팅(시드로 생성한 랜덤값을 넣는다.)

if문 비교 후 출력

5.Go Playground
구현하기 위해 go playground에서 시드값과 랜덤값 사이를 넣고 sha로 계산하면 값이 나온다.

level4에 sha값을 넣으면 성공한다.

6. 3줄 요약
- Go 바이너리는 런타임이 내장되어 있고 호출 규약이 독특하므로, Ghidra 스크립트와 플러그인을 활용해 심벌을 복원하고 인자 정보를 정리했다.
- 해당 CrackMe의 핵심 로직은 고정된 시드값(0x2e0ce0)**을 사용해 난수를 생성하고, 이를 SHA256으로 해싱하여 사용자 입력과 비교하는 것이다.
- 시드값이 고정되어 있어 생성되는 난수도 항상 같으므로, Go 코드로 동일한 로직을 구현해 해시값을 미리 계산하면 통과할 수 있다.
'Security > 리버싱' 카테고리의 다른 글
| [Ghidra] 리버스 엔지니어링 기드라 실전 가이드 스터디 - chapter 5(level3) (0) | 2026.01.24 |
|---|---|
| [Ghidra] 리버스 엔지니어링 기드라 실전 가이드 스터디 - 기드라 취약점 3 (0) | 2026.01.23 |
| [Ghidra] 리버스 엔지니어링 기드라 실전 가이드 스터디 - chapter 5(level2) (0) | 2026.01.22 |
| [Ghidra] 리버스 엔지니어링 기드라 실전 가이드 스터디 - chapter 5(level1) (0) | 2026.01.22 |
| [Ghidra] 리버스 엔지니어링 기드라 실전 가이드 스터디 - 00 (0) | 2026.01.04 |